|
| 1 | +#include <iostream> |
| 2 | +#include <git2.h> |
| 3 | +#include <termcolor/termcolor.hpp> |
| 4 | + |
| 5 | +#include "rebase_subcommand.hpp" |
| 6 | +#include "../utils/git_exception.hpp" |
| 7 | +#include "../wrapper/repository_wrapper.hpp" |
| 8 | +#include "../wrapper/signature_wrapper.hpp" |
| 9 | +#include "../wrapper/index_wrapper.hpp" |
| 10 | + |
| 11 | +rebase_subcommand::rebase_subcommand(const libgit2_object&, CLI::App& app) |
| 12 | +{ |
| 13 | + auto *sub = app.add_subcommand("rebase", "Reapply commits on top of another base tip"); |
| 14 | + |
| 15 | + sub->add_option("upstream", m_upstream, "Upstream branch to rebase onto"); |
| 16 | + sub->add_option("branch", m_branch, "Working branch; defaults to HEAD"); |
| 17 | + sub->add_option("--onto", m_onto, "Starting point at which to create the new commits"); |
| 18 | + |
| 19 | + sub->add_flag("--abort", m_abort, "Abort the rebase operation and reset HEAD to the original branch"); |
| 20 | + sub->add_flag("--continue", m_continue, "Restart the rebasing process after having resolved a merge conflict"); |
| 21 | + sub->add_flag("--skip", m_skip, "Restart the rebasing process by skipping the current patch"); |
| 22 | + sub->add_flag("--quit", m_quit, "Abort the rebase operation but HEAD is not reset back to the original branch"); |
| 23 | + |
| 24 | + sub->callback([this]() { this->run(); }); |
| 25 | +} |
| 26 | + |
| 27 | +void ensure_rebase_in_progress(git_repository_state_t state) |
| 28 | +{ |
| 29 | + if (state != GIT_REPOSITORY_STATE_REBASE_INTERACTIVE && |
| 30 | + state != GIT_REPOSITORY_STATE_REBASE_MERGE) |
| 31 | + { |
| 32 | + throw std::runtime_error("No rebase in progress"); |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +void rebase_subcommand::run() |
| 37 | +{ |
| 38 | + auto directory = get_current_git_path(); |
| 39 | + auto repo = repository_wrapper::open(directory); |
| 40 | + |
| 41 | + git_repository_state_t state = repo.state(); |
| 42 | + |
| 43 | + if (m_abort) |
| 44 | + { |
| 45 | + ensure_rebase_in_progress(state); |
| 46 | + run_abort(repo); |
| 47 | + return; |
| 48 | + } |
| 49 | + |
| 50 | + if (m_continue) |
| 51 | + { |
| 52 | + ensure_rebase_in_progress(state); |
| 53 | + run_continue(repo); |
| 54 | + return; |
| 55 | + } |
| 56 | + |
| 57 | + if (m_skip) |
| 58 | + { |
| 59 | + ensure_rebase_in_progress(state); |
| 60 | + run_skip(repo); |
| 61 | + return; |
| 62 | + } |
| 63 | + |
| 64 | + if (m_quit) |
| 65 | + { |
| 66 | + ensure_rebase_in_progress(state); |
| 67 | + run_quit(repo); |
| 68 | + return; |
| 69 | + } |
| 70 | + |
| 71 | + if (state == GIT_REPOSITORY_STATE_REBASE_INTERACTIVE || |
| 72 | + state == GIT_REPOSITORY_STATE_REBASE_MERGE) |
| 73 | + { |
| 74 | + throw std::runtime_error("A rebase is already in progress"); |
| 75 | + } |
| 76 | + |
| 77 | + if (state != GIT_REPOSITORY_STATE_NONE) |
| 78 | + { |
| 79 | + throw std::runtime_error("Cannot rebase: repository is in unexpected state"); |
| 80 | + } |
| 81 | + |
| 82 | + run_rebase(repo); |
| 83 | +} |
| 84 | + |
| 85 | +annotated_commit_wrapper rebase_subcommand::resolve_ref( |
| 86 | + const repository_wrapper& repo, |
| 87 | + const std::string& ref_name) const |
| 88 | +{ |
| 89 | + if (!ref_name.empty()) |
| 90 | + { |
| 91 | + auto branch_commit = repo.resolve_local_ref(m_branch); |
| 92 | + if (!branch_commit) |
| 93 | + { |
| 94 | + throw std::runtime_error("error: could not resolve branch '" + m_branch + "'"); |
| 95 | + } |
| 96 | + return std::move(branch_commit).value(); |
| 97 | + } |
| 98 | + else |
| 99 | + { |
| 100 | + auto head = repo.head(); |
| 101 | + auto branch_commit = repo.find_annotated_commit(head); |
| 102 | + return branch_commit; |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +void rebase_subcommand::perform_rebase(repository_wrapper& repo, rebase_wrapper& rebase) |
| 107 | +{ |
| 108 | + auto signatures = signature_wrapper::get_default_signature_from_env(repo); |
| 109 | + |
| 110 | + size_t total_operations = rebase.operation_entry_count(); |
| 111 | + std::cout << "Rebasing " << total_operations << " commit(s)..." << std::endl; |
| 112 | + |
| 113 | + while (rebase.next_operation()) |
| 114 | + { |
| 115 | + size_t current_idx = rebase.current_operation_index(); |
| 116 | + |
| 117 | + commit_wrapper original_commit = repo.find_commit(rebase.current_operation_id()); |
| 118 | + std::string commit_summary = original_commit.summary(); |
| 119 | + |
| 120 | + std::cout << "Applying: " << commit_summary << " (" |
| 121 | + << (current_idx + 1) << "/" << total_operations << ")" << std::endl; |
| 122 | + |
| 123 | + index_wrapper index = repo.make_index(); |
| 124 | + if (index.has_conflict()) |
| 125 | + { |
| 126 | + std:: cout << termcolor::red << "Conflicts detected!" << termcolor::reset << std::endl; |
| 127 | + std::cout << "Resolve conflicts and run:" << std::endl; |
| 128 | + std::cout << " git2cpp rebase --continue" << std::endl; |
| 129 | + std::cout << "or skip this commit with:" << std::endl; |
| 130 | + std::cout << " git2cpp rebase --skip" << std:: endl; |
| 131 | + std::cout << "or abort the rebase with:" << std:: endl; |
| 132 | + std::cout << " git2cpp rebase --abort" << std::endl; |
| 133 | + return; |
| 134 | + } |
| 135 | + |
| 136 | + int commit_result = rebase.commit(signatures.first, signatures.second); |
| 137 | + |
| 138 | + if (commit_result == GIT_EAPPLIED) |
| 139 | + { |
| 140 | + std::cout << termcolor::yellow << "Skipping commit (already applied)" |
| 141 | + << termcolor::reset << std::endl; |
| 142 | + } |
| 143 | + else |
| 144 | + { |
| 145 | + throw_if_error(commit_result); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + rebase.finish(signatures.second); |
| 150 | + |
| 151 | + std::cout << termcolor::green << "Successfully rebased and updated HEAD." |
| 152 | + << termcolor::reset << std::endl; |
| 153 | +} |
| 154 | + |
| 155 | +void rebase_subcommand::run_rebase(repository_wrapper& repo) |
| 156 | +{ |
| 157 | + if (m_upstream.empty()) |
| 158 | + { |
| 159 | + throw std::runtime_error("upstream is required for rebase"); |
| 160 | + } |
| 161 | + |
| 162 | + annotated_commit_wrapper branch = resolve_ref(repo, m_branch); |
| 163 | + std::optional<annotated_commit_wrapper> upstream = repo.resolve_local_ref(m_upstream); |
| 164 | + if (!upstream) |
| 165 | + { |
| 166 | + throw std::runtime_error("error: could not resolve upstream '" + m_upstream + "'"); |
| 167 | + } |
| 168 | + |
| 169 | + std::unique_ptr<annotated_commit_wrapper> onto_ptr = nullptr; |
| 170 | + if (!m_onto.empty()) |
| 171 | + { |
| 172 | + onto_ptr = std::make_unique<annotated_commit_wrapper>(resolve_ref(repo, m_onto)); |
| 173 | + } |
| 174 | + git_rebase_options rebase_opts ; |
| 175 | + throw_if_error(git_rebase_options_init(&rebase_opts, GIT_REBASE_OPTIONS_VERSION)); |
| 176 | + |
| 177 | + auto rebase = rebase_wrapper::init(repo, branch, upstream.value(), onto_ptr.get(), rebase_opts); |
| 178 | + perform_rebase(repo, rebase); |
| 179 | +} |
| 180 | + |
| 181 | +void rebase_subcommand::run_abort(repository_wrapper& repo) |
| 182 | +{ |
| 183 | + git_rebase_options rebase_opts; |
| 184 | + throw_if_error(git_rebase_options_init(&rebase_opts, GIT_REBASE_OPTIONS_VERSION)); |
| 185 | + auto rebase = rebase_wrapper::open(repo, rebase_opts); |
| 186 | + rebase.abort(); |
| 187 | + std::cout << "Rebase aborted" << std::endl; |
| 188 | +} |
| 189 | + |
| 190 | +void rebase_subcommand::run_continue(repository_wrapper& repo) |
| 191 | +{ |
| 192 | + // Check if there are still conflicts |
| 193 | + index_wrapper index = repo.make_index(); |
| 194 | + if (index.has_conflict()) |
| 195 | + { |
| 196 | + throw std::runtime_error("You must resolve conflicts before continuing the rebase"); |
| 197 | + } |
| 198 | + git_rebase_options rebase_opts; |
| 199 | + throw_if_error(git_rebase_options_init(&rebase_opts, GIT_REBASE_OPTIONS_VERSION)); |
| 200 | + auto rebase = rebase_wrapper::open(repo, rebase_opts); |
| 201 | + |
| 202 | + // Get signature for commits |
| 203 | + auto signatures = signature_wrapper::get_default_signature_from_env(repo); |
| 204 | + |
| 205 | + int commit_result = rebase.commit(signatures.first, signatures.second); |
| 206 | + |
| 207 | + if (commit_result == GIT_EAPPLIED) |
| 208 | + { |
| 209 | + std::cout << termcolor::yellow << "Skipping commit (already applied)" |
| 210 | + << termcolor::reset << std::endl; |
| 211 | + } |
| 212 | + else |
| 213 | + { |
| 214 | + throw_if_error(commit_result); |
| 215 | + } |
| 216 | + |
| 217 | + perform_rebase(repo, rebase); |
| 218 | +} |
| 219 | + |
| 220 | +void rebase_subcommand::run_skip(repository_wrapper& repo) |
| 221 | +{ |
| 222 | + git_rebase_options rebase_opts; |
| 223 | + throw_if_error(git_rebase_options_init(&rebase_opts, GIT_REBASE_OPTIONS_VERSION)); |
| 224 | + auto rebase = rebase_wrapper::open(repo, rebase_opts); |
| 225 | + |
| 226 | + std::cout << "Skipping current commit..." << std::endl; |
| 227 | + |
| 228 | + perform_rebase(repo, rebase); |
| 229 | +} |
| 230 | + |
| 231 | +void rebase_subcommand:: run_quit(repository_wrapper& repo) |
| 232 | +{ |
| 233 | + repo.state_cleanup(); |
| 234 | + std::cout << "Rebase state cleaned up (HEAD not reset)" << std::endl; |
| 235 | +} |
| 236 | + |
0 commit comments