Files
godot-git-plugin/godot-git-plugin/src/git_plugin.cpp
Fries 4cb6ec0edd fix a comparision bug with renamed git statuses
there is a bug where if you have a entry->status with GIT_STATUS_INDEX_RENAMED but with another flag like GIT_STATUS_INDEX_MODIFIED, godot-git-plugin will crash as it cant find the proper map for 2 flags. so i changed it to do a logical and so it can execute the proper renamed logic.
2023-05-18 23:28:08 -07:00

711 lines
30 KiB
C++

#include "git_plugin.h"
#include <cstring>
#include <git2/tree.h>
#include "godot_cpp/core/class_db.hpp"
#include "godot_cpp/classes/file_access.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#define GIT2_CALL(error, msg) \
if (check_errors(error, __FUNCTION__, __FILE__, __LINE__, msg)) { \
return; \
}
#define GIT2_CALL_R(error, msg, return_value) \
if (check_errors(error, __FUNCTION__, __FILE__, __LINE__, msg)) { \
return return_value; \
}
#define GIT2_CALL_IGNORE(error, msg, ignores) \
if (check_errors(error, __FUNCTION__, __FILE__, __LINE__, msg, ignores)) { \
return; \
}
#define GIT2_CALL_R_IGNORE(error, msg, return_value, ignores) \
if (check_errors(error, __FUNCTION__, __FILE__, __LINE__, msg, ignores)) { \
return return_value; \
}
#define COMMA ,
void GitPlugin::_bind_methods() {
// Doesn't seem to require binding functions for now
}
GitPlugin::GitPlugin() {
map_changes[GIT_STATUS_WT_NEW] = CHANGE_TYPE_NEW;
map_changes[GIT_STATUS_INDEX_NEW] = CHANGE_TYPE_NEW;
map_changes[GIT_STATUS_WT_MODIFIED] = CHANGE_TYPE_MODIFIED;
map_changes[GIT_STATUS_INDEX_MODIFIED] = CHANGE_TYPE_MODIFIED;
map_changes[GIT_STATUS_WT_RENAMED] = CHANGE_TYPE_RENAMED;
map_changes[GIT_STATUS_INDEX_RENAMED] = CHANGE_TYPE_RENAMED;
map_changes[GIT_STATUS_WT_DELETED] = CHANGE_TYPE_DELETED;
map_changes[GIT_STATUS_INDEX_DELETED] = CHANGE_TYPE_DELETED;
map_changes[GIT_STATUS_WT_TYPECHANGE] = CHANGE_TYPE_TYPECHANGE;
map_changes[GIT_STATUS_INDEX_TYPECHANGE] = CHANGE_TYPE_TYPECHANGE;
map_changes[GIT_STATUS_CONFLICTED] = CHANGE_TYPE_UNMERGED;
}
bool GitPlugin::check_errors(int error, godot::String function, godot::String file, int line, godot::String message, const std::vector<git_error_code> &ignores) {
const git_error *lg2err;
if (error == 0) {
return false;
}
for (auto &ig : ignores) {
if (error == ig) {
return false;
}
}
message = message + ".";
if ((lg2err = git_error_last()) != nullptr && lg2err->message != nullptr) {
message = message + " Error " + godot::String::num_int64(error) + ": ";
message = message + godot::String(lg2err->message);
}
godot::UtilityFunctions::push_error("GitPlugin: ", CString(message).data, " in ", CString(file).data, ":", CString(function).data, "#L", line);
return true;
}
void GitPlugin::_set_credentials(const godot::String &username, const godot::String &password, const godot::String &ssh_public_key_path, const godot::String &ssh_private_key_path, const godot::String &ssh_passphrase) {
creds.username = username;
creds.password = password;
creds.ssh_public_key_path = ssh_public_key_path;
creds.ssh_private_key_path = ssh_private_key_path;
creds.ssh_passphrase = ssh_passphrase;
}
void GitPlugin::_discard_file(const godot::String &file_path) {
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
CString c_path(file_path);
char *paths[] = { c_path.data };
opts.paths = { paths, 1 };
opts.checkout_strategy = GIT_CHECKOUT_FORCE;
GIT2_CALL(git_checkout_index(repo.get(), nullptr, &opts), "Could not checkout index");
}
void GitPlugin::_commit(const godot::String &msg) {
git_index_ptr repo_index;
GIT2_CALL(git_repository_index(Capture(repo_index), repo.get()), "Could not get repository index");
git_oid tree_id;
GIT2_CALL(git_index_write_tree(&tree_id, repo_index.get()), "Could not write index to tree");
GIT2_CALL(git_index_write(repo_index.get()), "Could not write index to disk");
git_tree_ptr tree;
GIT2_CALL(git_tree_lookup(Capture(tree), repo.get(), &tree_id), "Could not lookup tree from ID");
git_signature_ptr default_sign;
GIT2_CALL(git_signature_default(Capture(default_sign), repo.get()), "Could not get default signature");
git_oid parent_commit_id = {};
GIT2_CALL_IGNORE(git_reference_name_to_id(&parent_commit_id, repo.get(), "HEAD"), "Could not get repository HEAD", { GIT_ENOTFOUND });
git_commit_ptr parent_commit;
if (!git_oid_is_zero(&parent_commit_id)) {
GIT2_CALL(git_commit_lookup(Capture(parent_commit), repo.get(), &parent_commit_id), "Could not lookup parent commit data");
}
git_oid new_commit_id;
if (!has_merge) {
GIT2_CALL(
git_commit_create_v(
&new_commit_id,
repo.get(),
"HEAD",
default_sign.get(),
default_sign.get(),
"UTF-8",
CString(msg).data,
tree.get(),
parent_commit.get() ? 1 : 0,
parent_commit.get()),
"Could not create commit");
} else {
git_commit_ptr fetchhead_commit;
GIT2_CALL(git_commit_lookup(Capture(fetchhead_commit), repo.get(), &pull_merge_oid), "Could not lookup commit pointed to by HEAD");
GIT2_CALL(
git_commit_create_v(
&new_commit_id,
repo.get(),
"HEAD",
default_sign.get(),
default_sign.get(),
"UTF-8",
CString(msg).data,
tree.get(),
2,
parent_commit.get(),
fetchhead_commit.get()),
"Could not create merge commit");
has_merge = false;
GIT2_CALL(git_repository_state_cleanup(repo.get()), "Could not clean repository state");
}
}
void GitPlugin::_stage_file(const godot::String &file_path) {
CString c_path(file_path);
char *paths[] = { c_path.data };
git_strarray array = { paths, 1 };
git_index_ptr index;
GIT2_CALL(git_repository_index(Capture(index), repo.get()), "Could not get repository index");
GIT2_CALL(git_index_add_all(index.get(), &array, GIT_INDEX_ADD_DEFAULT | GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH, nullptr, nullptr), "Could not add " + file_path + " to index");
GIT2_CALL(git_index_write(index.get()), "Could not write changes to disk");
}
void GitPlugin::_unstage_file(const godot::String &file_path) {
CString c_path(file_path);
char *paths[] = { c_path.data };
git_strarray array = { paths, 1 };
git_reference_ptr head;
GIT2_CALL_IGNORE(git_repository_head(Capture(head), repo.get()), "Could not find repository HEAD", { GIT_ENOTFOUND COMMA GIT_EUNBORNBRANCH });
if (head) {
git_object_ptr head_commit;
GIT2_CALL(git_reference_peel(Capture(head_commit), head.get(), GIT_OBJ_COMMIT), "Could not peel HEAD reference");
GIT2_CALL(git_reset_default(repo.get(), head_commit.get(), &array), "Could not reset " + file_path + " to state at HEAD");
} else {
// If there is no HEAD commit, we should just remove the file from the index.
CString c_path(file_path);
git_index_ptr index;
GIT2_CALL(git_repository_index(Capture(index), repo.get()), "Could not get repository index");
GIT2_CALL(git_index_remove_bypath(index.get(), c_path.data), "Could not add " + file_path + " to index");
GIT2_CALL(git_index_write(index.get()), "Could not write changes to disk");
}
}
void GitPlugin::create_gitignore_and_gitattributes() {
if (!godot::FileAccess::file_exists(repo_project_path + "/.gitignore")) {
godot::Ref<godot::FileAccess> file = godot::FileAccess::open(repo_project_path + "/.gitignore", godot::FileAccess::ModeFlags::WRITE);
ERR_FAIL_COND(file.is_null());
file->store_string(
"# Godot 4+ specific ignores\n"
".godot/\n");
}
if (!godot::FileAccess::file_exists(repo_project_path + "/.gitattributes")) {
godot::Ref<godot::FileAccess> file = godot::FileAccess::open(repo_project_path + "/.gitattributes", godot::FileAccess::ModeFlags::WRITE);
ERR_FAIL_COND(file.is_null());
file->store_string(
"# Set the default behavior, in case people don't have core.autocrlf set.\n"
"* text=auto\n\n"
"# Explicitly declare text files you want to always be normalized and converted\n"
"# to native line endings on checkout.\n"
"*.cpp text\n"
"*.c text\n"
"*.h text\n"
"*.gd text\n"
"*.cs text\n\n"
"# Declare files that will always have CRLF line endings on checkout.\n"
"*.sln text eol=crlf\n\n"
"# Denote all files that are truly binary and should not be modified.\n"
"*.png binary\n"
"*.jpg binary\n");
}
}
godot::TypedArray<godot::Dictionary> GitPlugin::_get_modified_files_data() {
godot::TypedArray<godot::Dictionary> stats_files;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
opts.flags = GIT_STATUS_OPT_EXCLUDE_SUBMODULES;
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
git_status_list_ptr statuses;
GIT2_CALL_R(git_status_list_new(Capture(statuses), repo.get(), &opts), "Could not get status information from repository", godot::TypedArray<godot::Dictionary>());
size_t count = git_status_list_entrycount(statuses.get());
for (size_t i = 0; i < count; ++i) {
const git_status_entry *entry = git_status_byindex(statuses.get(), i);
godot::String path;
if (entry->index_to_workdir) {
path = entry->index_to_workdir->new_file.path;
} else {
path = entry->head_to_index->new_file.path;
}
const static int git_status_wt = GIT_STATUS_WT_NEW | GIT_STATUS_WT_MODIFIED | GIT_STATUS_WT_DELETED | GIT_STATUS_WT_TYPECHANGE | GIT_STATUS_WT_RENAMED | GIT_STATUS_CONFLICTED;
const static int git_status_index = GIT_STATUS_INDEX_NEW | GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_INDEX_DELETED | GIT_STATUS_INDEX_RENAMED | GIT_STATUS_INDEX_TYPECHANGE;
if (entry->status & git_status_wt) {
stats_files.push_back(create_status_file(path, map_changes[git_status_t(entry->status & git_status_wt)], TREE_AREA_UNSTAGED));
}
if (entry->status & git_status_index) {
if (entry->status & GIT_STATUS_INDEX_RENAMED) {
godot::String old_path = entry->head_to_index->old_file.path;
stats_files.push_back(create_status_file(old_path, map_changes.at(GIT_STATUS_INDEX_DELETED), TREE_AREA_STAGED));
stats_files.push_back(create_status_file(path, map_changes.at(GIT_STATUS_INDEX_NEW), TREE_AREA_STAGED));
} else {
stats_files.push_back(create_status_file(path, map_changes.at(git_status_t(entry->status & git_status_index)), TREE_AREA_STAGED));
}
}
}
return stats_files;
}
godot::TypedArray<godot::String> GitPlugin::_get_branch_list() {
git_branch_iterator_ptr it;
GIT2_CALL_R(git_branch_iterator_new(Capture(it), repo.get(), GIT_BRANCH_LOCAL), "Could not create branch iterator", godot::TypedArray<godot::Dictionary>());
godot::TypedArray<godot::String> branch_names;
git_reference_ptr ref;
git_branch_t type;
while (git_branch_next(Capture(ref), &type, it.get()) != GIT_ITEROVER) {
const char *name = nullptr;
GIT2_CALL_R(git_branch_name(&name, ref.get()), "Could not get branch name", godot::TypedArray<godot::String>());
if (git_branch_is_head(ref.get())) {
// Always send the current branch as the first branch in list
branch_names.push_front(name);
} else {
branch_names.push_back(godot::String(name));
}
}
return branch_names;
}
void GitPlugin::_create_branch(const godot::String &branch_name) {
git_oid head_commit_id;
GIT2_CALL(git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD"), "Could not get HEAD commit ID");
git_commit_ptr head_commit;
GIT2_CALL(git_commit_lookup(Capture(head_commit), repo.get(), &head_commit_id), "Could not lookup HEAD commit");
git_reference_ptr branch_ref;
GIT2_CALL(git_branch_create(Capture(branch_ref), repo.get(), CString(branch_name).data, head_commit.get(), 0), "Could not create branch from HEAD");
}
void GitPlugin::_create_remote(const godot::String &remote_name, const godot::String &remote_url) {
git_remote_ptr remote;
GIT2_CALL(git_remote_create(Capture(remote), repo.get(), CString(remote_name).data, CString(remote_url).data), "Could not create remote");
}
void GitPlugin::_remove_branch(const godot::String &branch_name) {
git_reference_ptr branch;
GIT2_CALL(git_branch_lookup(Capture(branch), repo.get(), CString(branch_name).data, GIT_BRANCH_LOCAL), "Could not find branch " + branch_name);
GIT2_CALL(git_branch_delete(branch.get()), "Could not delete branch reference of " + branch_name);
}
void GitPlugin::_remove_remote(const godot::String &remote_name) {
GIT2_CALL(git_remote_delete(repo.get(), CString(remote_name).data), "Could not delete remote " + remote_name);
}
godot::TypedArray<godot::Dictionary> GitPlugin::_get_line_diff(const godot::String &file_path, const godot::String &text) {
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
opts.context_lines = 0;
opts.flags = GIT_DIFF_DISABLE_PATHSPEC_MATCH | GIT_DIFF_INCLUDE_UNTRACKED;
git_index_ptr index;
GIT2_CALL_R(git_repository_index(Capture(index), repo.get()), "Failed to get repository index", godot::TypedArray<godot::Dictionary>());
GIT2_CALL_R(git_index_read(index.get(), 0), "Failed to read index", godot::TypedArray<godot::Dictionary>());
const git_index_entry *entry = git_index_get_bypath(index.get(), CString(file_path).data, GIT_INDEX_STAGE_NORMAL);
if (!entry) {
return godot::TypedArray<godot::Dictionary>();
}
git_reference_ptr head;
GIT2_CALL_R(git_repository_head(Capture(head), repo.get()), "Failed to load repository head", godot::TypedArray<godot::Dictionary>());
git_blob_ptr blob;
GIT2_CALL_R(git_blob_lookup(Capture(blob), repo.get(), &entry->id), "Failed to load head blob", godot::TypedArray<godot::Dictionary>());
godot::TypedArray<godot::Dictionary> diff_contents;
DiffHelper diff_helper = { &diff_contents, this };
GIT2_CALL_R(git_diff_blob_to_buffer(blob.get(), nullptr, CString(text).data, text.length(), nullptr, &opts, nullptr, nullptr, diff_hunk_cb, nullptr, &diff_helper), "Failed to make diff", godot::TypedArray<godot::Dictionary>());
return diff_contents;
}
godot::String GitPlugin::_get_current_branch_name() {
git_reference_ptr head;
GIT2_CALL_R_IGNORE(git_repository_head(Capture(head), repo.get()), "Could not find repository HEAD", "", { GIT_ENOTFOUND COMMA GIT_EUNBORNBRANCH });
if (!head) {
// We are likely at a state where the initial commit hasn't been made yet.
return "";
}
git_reference_ptr branch;
GIT2_CALL_R(git_reference_resolve(Capture(branch), head.get()), "Could not resolve HEAD reference", "");
const char *name = "";
GIT2_CALL_R(git_branch_name(&name, branch.get()), "Could not get branch name from current branch reference", "");
return name;
}
godot::TypedArray<godot::String> GitPlugin::_get_remotes() {
git_strarray remote_array;
GIT2_CALL_R(git_remote_list(&remote_array, repo.get()), "Could not get list of remotes", godot::TypedArray<godot::Dictionary>());
godot::TypedArray<godot::String> remotes;
for (int i = 0; i < remote_array.count; i++) {
remotes.push_back(remote_array.strings[i]);
}
return remotes;
}
godot::TypedArray<godot::Dictionary> GitPlugin::_get_previous_commits(int32_t max_commits) {
git_revwalk_ptr walker;
GIT2_CALL_R(git_revwalk_new(Capture(walker), repo.get()), "Could not create new revwalk", godot::TypedArray<godot::Dictionary>());
GIT2_CALL_R(git_revwalk_sorting(walker.get(), GIT_SORT_TIME), "Could not sort revwalk by time", godot::TypedArray<godot::Dictionary>());
GIT2_CALL_R_IGNORE(git_revwalk_push_head(walker.get()), "Could not push HEAD to revwalk", godot::TypedArray<godot::Dictionary>(), { GIT_ENOTFOUND COMMA GIT_ERROR });
git_oid oid;
godot::TypedArray<godot::Dictionary> commits;
char commit_id[GIT_OID_HEXSZ + 1];
for (int i = 0; !git_revwalk_next(&oid, walker.get()) && i <= max_commits; i++) {
git_commit_ptr commit;
GIT2_CALL_R(git_commit_lookup(Capture(commit), repo.get(), &oid), "Failed to lookup the commit", commits);
git_oid_tostr(commit_id, GIT_OID_HEXSZ + 1, git_commit_id(commit.get()));
godot::String msg = git_commit_message(commit.get());
const git_signature *sig = git_commit_author(commit.get());
godot::String author = godot::String() + sig->name + " <" + sig->email + ">";
commits.push_back(create_commit(msg, author, commit_id, sig->when.time, sig->when.offset));
}
return commits;
}
void GitPlugin::_fetch(const godot::String &remote) {
godot::UtilityFunctions::print("GitPlugin: Performing fetch from ", CString(remote).data);
git_remote_ptr remote_object;
GIT2_CALL(git_remote_lookup(Capture(remote_object), repo.get(), CString(remote).data), "Could not lookup remote \"" + remote + "\"");
git_remote_callbacks remote_cbs = GIT_REMOTE_CALLBACKS_INIT;
remote_cbs.credentials = &credentials_cb;
remote_cbs.update_tips = &update_cb;
remote_cbs.sideband_progress = &progress_cb;
remote_cbs.transfer_progress = &transfer_progress_cb;
remote_cbs.payload = &creds;
remote_cbs.push_transfer_progress = &push_transfer_progress_cb;
remote_cbs.push_update_reference = &push_update_reference_cb;
GIT2_CALL(git_remote_connect(remote_object.get(), GIT_DIRECTION_FETCH, &remote_cbs, nullptr, nullptr), "Could not connect to remote \"" + remote + "\". Are your credentials correct? Try using a PAT token (in case you are using Github) as your password");
git_fetch_options opts = GIT_FETCH_OPTIONS_INIT;
opts.callbacks = remote_cbs;
GIT2_CALL(git_remote_fetch(remote_object.get(), nullptr, &opts, "fetch"), "Could not fetch data from remote");
godot::UtilityFunctions::print("GitPlugin: Fetch ended");
}
void GitPlugin::_pull(const godot::String &remote) {
godot::UtilityFunctions::print("GitPlugin: Performing pull from ", CString(remote).data);
git_remote_ptr remote_object;
GIT2_CALL(git_remote_lookup(Capture(remote_object), repo.get(), CString(remote).data), "Could not lookup remote \"" + remote + "\"");
git_remote_callbacks remote_cbs = GIT_REMOTE_CALLBACKS_INIT;
remote_cbs.credentials = &credentials_cb;
remote_cbs.update_tips = &update_cb;
remote_cbs.sideband_progress = &progress_cb;
remote_cbs.transfer_progress = &transfer_progress_cb;
remote_cbs.payload = &creds;
remote_cbs.push_transfer_progress = &push_transfer_progress_cb;
remote_cbs.push_update_reference = &push_update_reference_cb;
GIT2_CALL(git_remote_connect(remote_object.get(), GIT_DIRECTION_FETCH, &remote_cbs, nullptr, nullptr), "Could not connect to remote \"" + remote + "\". Are your credentials correct? Try using a PAT token (in case you are using Github) as your password");
git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
fetch_opts.callbacks = remote_cbs;
godot::String branch_name = _get_current_branch_name();
CString ref_spec_str("refs/heads/" + branch_name);
char *ref[] = { ref_spec_str.data };
git_strarray refspec = { ref, 1 };
GIT2_CALL(git_remote_fetch(remote_object.get(), &refspec, &fetch_opts, "pull"), "Could not fetch data from remote");
pull_merge_oid = {};
GIT2_CALL(git_repository_fetchhead_foreach(repo.get(), fetchhead_foreach_cb, &pull_merge_oid), "Could not read \"FETCH_HEAD\" file");
if (git_oid_is_zero(&pull_merge_oid)) {
godot::UtilityFunctions::push_error("GitPlugin: Could not find remote branch HEAD for " + branch_name + ". Try pushing the branch first.");
return;
}
git_annotated_commit_ptr fetchhead_annotated_commit;
GIT2_CALL(git_annotated_commit_lookup(Capture(fetchhead_annotated_commit), repo.get(), &pull_merge_oid), "Could not get merge commit");
const git_annotated_commit *merge_heads[] = { fetchhead_annotated_commit.get() };
git_merge_analysis_t merge_analysis;
git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE;
GIT2_CALL(git_merge_analysis(&merge_analysis, &preference, repo.get(), merge_heads, 1), "Merge analysis failed");
if (merge_analysis & GIT_MERGE_ANALYSIS_FASTFORWARD) {
git_checkout_options ff_checkout_options = GIT_CHECKOUT_OPTIONS_INIT;
int err = 0;
git_reference_ptr target_ref;
GIT2_CALL(git_repository_head(Capture(target_ref), repo.get()), "Failed to get HEAD reference");
git_object_ptr target;
GIT2_CALL(git_object_lookup(Capture(target), repo.get(), &pull_merge_oid, GIT_OBJECT_COMMIT), "Failed to lookup OID " + godot::String(git_oid_tostr_s(&pull_merge_oid)));
ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE;
GIT2_CALL(git_checkout_tree(repo.get(), target.get(), &ff_checkout_options), "Failed to checkout HEAD reference");
git_reference_ptr new_target_ref;
GIT2_CALL(git_reference_set_target(Capture(new_target_ref), target_ref.get(), &pull_merge_oid, nullptr), "Failed to move HEAD reference");
godot::UtilityFunctions::print("GitPlugin: Fast Forwarded");
GIT2_CALL(git_repository_state_cleanup(repo.get()), "Could not clean repository state");
} else if (merge_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.file_favor = GIT_MERGE_FILE_FAVOR_NORMAL;
merge_opts.file_flags = (GIT_MERGE_FILE_STYLE_DIFF3 | GIT_MERGE_FILE_DIFF_MINIMAL);
checkout_opts.checkout_strategy = (GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS | GIT_CHECKOUT_CONFLICT_STYLE_MERGE);
GIT2_CALL(git_merge(repo.get(), merge_heads, 1, &merge_opts, &checkout_opts), "Merge Failed");
git_index_ptr index;
GIT2_CALL(git_repository_index(Capture(index), repo.get()), "Could not get repository index");
if (git_index_has_conflicts(index.get())) {
godot::UtilityFunctions::push_error("GitPlugin: Index has conflicts. Solve conflicts and make a merge commit.");
} else {
godot::UtilityFunctions::push_error("GitPlugin: Changes are staged. Make a merge commit.");
}
has_merge = true;
} else if (merge_analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
godot::UtilityFunctions::print("GitPlugin: Already up to date");
GIT2_CALL(git_repository_state_cleanup(repo.get()), "Could not clean repository state");
} else {
godot::UtilityFunctions::push_error("GitPlugin: Can not merge");
}
godot::UtilityFunctions::print("GitPlugin: Pull ended");
}
void GitPlugin::_push(const godot::String &remote, bool force) {
godot::UtilityFunctions::print("GitPlugin: Performing push to ", CString(remote).data);
git_remote_ptr remote_object;
GIT2_CALL(git_remote_lookup(Capture(remote_object), repo.get(), CString(remote).data), "Could not lookup remote \"" + remote + "\"");
git_remote_callbacks remote_cbs = GIT_REMOTE_CALLBACKS_INIT;
remote_cbs.credentials = &credentials_cb;
remote_cbs.update_tips = &update_cb;
remote_cbs.sideband_progress = &progress_cb;
remote_cbs.transfer_progress = &transfer_progress_cb;
remote_cbs.payload = &creds;
remote_cbs.push_transfer_progress = &push_transfer_progress_cb;
remote_cbs.push_update_reference = &push_update_reference_cb;
GIT2_CALL(git_remote_connect(remote_object.get(), GIT_DIRECTION_PUSH, &remote_cbs, nullptr, nullptr), "Could not connect to remote \"" + remote + "\". Are your credentials correct? Try using a PAT token (in case you are using Github) as your password");
godot::String branch_name = _get_current_branch_name();
CString pushspec(godot::String() + (force ? "+" : "") + "refs/heads/" + branch_name);
const git_strarray refspec = { &pushspec.data, 1 };
git_push_options push_options = GIT_PUSH_OPTIONS_INIT;
push_options.callbacks = remote_cbs;
GIT2_CALL(git_remote_push(remote_object.get(), &refspec, &push_options), "Failed to push");
godot::UtilityFunctions::print("GitPlugin: Push ended");
}
bool GitPlugin::_checkout_branch(const godot::String &branch_name) {
git_reference_ptr branch;
GIT2_CALL_R(git_branch_lookup(Capture(branch), repo.get(), CString(branch_name).data, GIT_BRANCH_LOCAL), "Could not find branch", false);
const char *branch_ref_name = git_reference_name(branch.get());
git_object_ptr treeish;
GIT2_CALL_R(git_revparse_single(Capture(treeish), repo.get(), CString(branch_name).data), "Could not find branch head", false);
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
opts.checkout_strategy = GIT_CHECKOUT_SAFE;
GIT2_CALL_R(git_checkout_tree(repo.get(), treeish.get(), &opts), "Could not checkout branch tree", false);
GIT2_CALL_R(git_repository_set_head(repo.get(), branch_ref_name), "Could not set head", false);
return true;
}
godot::TypedArray<godot::Dictionary> GitPlugin::_get_diff(const godot::String &identifier, const int32_t area) {
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
godot::TypedArray<godot::Dictionary> diff_contents;
opts.context_lines = 2;
opts.interhunk_lines = 0;
opts.flags = GIT_DIFF_RECURSE_UNTRACKED_DIRS | GIT_DIFF_DISABLE_PATHSPEC_MATCH | GIT_DIFF_INCLUDE_UNTRACKED | GIT_DIFF_SHOW_UNTRACKED_CONTENT | GIT_DIFF_INCLUDE_TYPECHANGE;
CString pathspec(identifier);
opts.pathspec.strings = &pathspec.data;
opts.pathspec.count = 1;
git_diff_ptr diff;
switch ((TreeArea)area) {
case TREE_AREA_UNSTAGED: {
GIT2_CALL_R(git_diff_index_to_workdir(Capture(diff), repo.get(), nullptr, &opts), "Could not create diff for index from working directory", diff_contents);
} break;
case TREE_AREA_STAGED: {
git_object_ptr obj;
// Ignore the case when HEAD is not found. We need to compare with a null tree in the case where the HEAD reference object is empty.
GIT2_CALL_R_IGNORE(git_revparse_single(Capture(obj), repo.get(), "HEAD^{tree}"), "Could not get HEAD^{tree} object", diff_contents, { GIT_ENOTFOUND });
git_tree_ptr tree;
if (obj) {
GIT2_CALL_R_IGNORE(git_tree_lookup(Capture(tree), repo.get(), git_object_id(obj.get())), "Could not lookup HEAD^{tree}", diff_contents, { GIT_ENOTFOUND });
}
GIT2_CALL_R(git_diff_tree_to_index(Capture(diff), repo.get(), tree.get(), nullptr, &opts), "Could not create diff for tree from index directory", diff_contents);
} break;
case TREE_AREA_COMMIT: {
opts.pathspec = {};
git_object_ptr obj;
GIT2_CALL_R(git_revparse_single(Capture(obj), repo.get(), pathspec.data), "Could not get object at " + identifier, diff_contents);
git_commit_ptr commit;
GIT2_CALL_R(git_commit_lookup(Capture(commit), repo.get(), git_object_id(obj.get())), "Could not get commit " + identifier, diff_contents);
git_commit_ptr parent;
// We ignore the case when the parent is not found to handle the case when this commit is the root commit. We only need to diff against a null tree in that case.
GIT2_CALL_R_IGNORE(git_commit_parent(Capture(parent), commit.get(), 0), "Could not get parent commit of " + identifier, diff_contents, { GIT_ENOTFOUND });
git_tree_ptr commit_tree;
GIT2_CALL_R(git_commit_tree(Capture(commit_tree), commit.get()), "Could not get commit tree of " + identifier, diff_contents);
git_tree_ptr parent_tree;
if (parent) {
GIT2_CALL_R(git_commit_tree(Capture(parent_tree), parent.get()), "Could not get commit tree of " + identifier, diff_contents);
}
GIT2_CALL_R(git_diff_tree_to_tree(Capture(diff), repo.get(), parent_tree.get(), commit_tree.get(), &opts), "Could not generate diff for commit " + identifier, diff_contents);
} break;
}
diff_contents = _parse_diff(diff.get());
return diff_contents;
}
godot::TypedArray<godot::Dictionary> GitPlugin::_parse_diff(git_diff *diff) {
godot::TypedArray<godot::Dictionary> diff_contents;
for (int i = 0; i < git_diff_num_deltas(diff); i++) {
const git_diff_delta *delta = git_diff_get_delta(diff, i);
git_patch_ptr patch;
GIT2_CALL_R(git_patch_from_diff(Capture(patch), diff, i), "Could not create patch from diff", godot::TypedArray<godot::Dictionary>());
godot::Dictionary diff_file = create_diff_file(delta->new_file.path, delta->old_file.path);
godot::TypedArray<godot::Dictionary> diff_hunks;
for (int j = 0; j < git_patch_num_hunks(patch.get()); j++) {
const git_diff_hunk *git_hunk;
size_t line_count;
GIT2_CALL_R(git_patch_get_hunk(&git_hunk, &line_count, patch.get(), j), "Could not get hunk from patch", godot::TypedArray<godot::Dictionary>());
godot::Dictionary diff_hunk = create_diff_hunk(git_hunk->old_start, git_hunk->new_start, git_hunk->old_lines, git_hunk->new_lines);
godot::TypedArray<godot::Dictionary> diff_lines;
for (int k = 0; k < line_count; k++) {
const git_diff_line *git_diff_line;
GIT2_CALL_R(git_patch_get_line_in_hunk(&git_diff_line, patch.get(), j, k), "Could not get line from hunk in patch", godot::TypedArray<godot::Dictionary>());
char *content = new char[git_diff_line->content_len + 1];
std::memcpy(content, git_diff_line->content, git_diff_line->content_len);
content[git_diff_line->content_len] = '\0';
godot::String status = " "; // We reserve 1 null terminated space to fill the + or the - character at git_diff_line->origin
status[0] = git_diff_line->origin;
diff_lines.push_back(create_diff_line(git_diff_line->new_lineno, git_diff_line->old_lineno, godot::String(content), status));
delete[] content;
}
diff_hunk = add_line_diffs_into_diff_hunk(diff_hunk, diff_lines);
diff_hunks.push_back(diff_hunk);
}
diff_file = add_diff_hunks_into_diff_file(diff_file, diff_hunks);
diff_contents.push_back(diff_file);
}
return diff_contents;
}
godot::String GitPlugin::_get_vcs_name() {
return "Git";
}
bool GitPlugin::_initialize(const godot::String &project_path) {
using namespace godot;
ERR_FAIL_COND_V(project_path == "", false);
int init = git_libgit2_init();
if (init > 1) {
WARN_PRINT("Multiple libgit2 instances are running");
}
git_buf discovered_repo_path = {};
if (git_repository_discover(&discovered_repo_path, CString(project_path).data, 1, nullptr) == 0) {
repo_project_path = godot::String(discovered_repo_path.ptr);
godot::UtilityFunctions::print("Found a repository at " + godot::String(discovered_repo_path.ptr) + ".");
git_buf_dispose(&discovered_repo_path);
} else {
repo_project_path = project_path;
godot::UtilityFunctions::push_warning("Could not find any higher level repositories.");
}
godot::UtilityFunctions::print("Selected repository path: " + godot::String(repo_project_path) + ".");
GIT2_CALL_R(git_repository_init(Capture(repo), CString(repo_project_path).data, 0), "Could not initialize repository", false);
git_reference_ptr head;
GIT2_CALL_R_IGNORE(git_repository_head(Capture(head), repo.get()), "Could not get repository HEAD", false, { GIT_EUNBORNBRANCH COMMA GIT_ENOTFOUND });
if (!head) {
create_gitignore_and_gitattributes();
}
return true;
}
bool GitPlugin::_shut_down() {
repo.reset(); // Destroy repo object before libgit2 shuts down
GIT2_CALL_R(git_libgit2_shutdown(), "Could not shutdown Git Plugin", false);
return true;
}