-
Notifications
You must be signed in to change notification settings - Fork 5
Add checkout file
#165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add checkout file
#165
Changes from 4 commits
fcbd9be
1136abe
a16dc5e
f5874fc
db9fe56
8d4fda3
70a9065
3cb4e11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| #include "../subcommand/checkout_subcommand.hpp" | ||
|
|
||
| #include <filesystem> | ||
| #include <iostream> | ||
| #include <set> | ||
| #include <sstream> | ||
|
|
||
| #include <git2/oid.h> | ||
|
|
||
| #include "../subcommand/status_subcommand.hpp" | ||
| #include "../utils/git_exception.hpp" | ||
|
|
@@ -13,7 +15,10 @@ checkout_subcommand::checkout_subcommand(const libgit2_object&, CLI::App& app) | |
| { | ||
| auto* sub = app.add_subcommand("checkout", "Switch branches or restore working tree files"); | ||
|
|
||
| sub->add_option("<branch>", m_branch_name, "Branch to checkout"); | ||
| // "-- file" lands in m_positional_args because CLI11 consumes "--" silently. | ||
| sub->add_option("<tree-ish|pathspec>", m_positional_args, "Tree-ish to checkout, and/or one/many pathspec(s)"); | ||
| // checkout <branch>, checkout <tag>, checkout <file> ..., checkout <branch> <file> ... | ||
| // Use without "--" | ||
| sub->add_flag("-b", m_create_flag, "Create a new branch before checking it out"); | ||
| sub->add_flag("-B", m_force_create_flag, "Create a new branch or reset it if it exists before checking it out"); | ||
| sub->add_flag( | ||
|
|
@@ -51,6 +56,56 @@ namespace | |
| } | ||
| } | ||
|
|
||
| void checkout_subcommand::checkout_files( | ||
| const repository_wrapper& repo, | ||
| const std::vector<std::string>& files, | ||
| const git_checkout_options& base_options | ||
| ) | ||
| { | ||
| std::vector<const char*> pathspec_strings; | ||
| pathspec_strings.reserve(files.size()); | ||
| for (const auto& f : files) | ||
| { | ||
| pathspec_strings.push_back(f.c_str()); | ||
| } | ||
|
JohanMabille marked this conversation as resolved.
|
||
|
|
||
| git_checkout_options options = base_options; | ||
| options.paths.strings = const_cast<char**>(pathspec_strings.data()); | ||
| options.paths.count = pathspec_strings.size(); | ||
|
|
||
| throw_if_error(git_checkout_head(repo, &options)); | ||
| } | ||
|
|
||
| void checkout_subcommand::checkout_paths( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be renamed |
||
| const repository_wrapper& repo, | ||
| const std::string_view tree_ish, | ||
| const std::vector<std::string>& pathspecs, | ||
| const git_checkout_options& base_options | ||
| ) | ||
| { | ||
| auto obj = repo.revparse_single(tree_ish); | ||
| if (!obj) | ||
| { | ||
| throw git_exception( | ||
| "error: could not resolve tree-ish '" + std::string(tree_ish) + "'", | ||
| git2cpp_error_code::BAD_ARGUMENT | ||
| ); | ||
| } | ||
|
|
||
| std::vector<const char*> pathspec_strings; | ||
| pathspec_strings.reserve(pathspecs.size()); | ||
| for (const auto& p : pathspecs) | ||
| { | ||
| pathspec_strings.push_back(p.c_str()); | ||
| } | ||
|
|
||
| git_checkout_options options = base_options; | ||
| options.paths.strings = const_cast<char**>(pathspec_strings.data()); | ||
| options.paths.count = pathspec_strings.size(); | ||
|
|
||
| throw_if_error(git_checkout_tree(repo, *obj, &options)); | ||
| } | ||
|
|
||
| void checkout_subcommand::run() | ||
| { | ||
| auto directory = get_current_git_path(); | ||
|
|
@@ -73,57 +128,134 @@ void checkout_subcommand::run() | |
| options.checkout_strategy = GIT_CHECKOUT_SAFE; | ||
| } | ||
|
|
||
| if (m_create_flag || m_force_create_flag) | ||
| if (m_positional_args.empty()) | ||
| { | ||
| auto annotated_commit = create_local_branch(repo, m_branch_name, m_force_create_flag); | ||
| checkout_tree(repo, annotated_commit, m_branch_name, options); | ||
| update_head(repo, annotated_commit, m_branch_name); | ||
|
|
||
| std::cout << "Switched to a new branch '" << m_branch_name << "'" << std::endl; | ||
| throw std::runtime_error("error: no branch or file specified"); | ||
| } | ||
| else | ||
|
|
||
| const std::string& target_name = m_positional_args[0]; // can be a branch or a tag | ||
| const std::vector<std::string> pathspecs(m_positional_args.begin() + 1, m_positional_args.end()); | ||
|
|
||
| if (m_create_flag || m_force_create_flag) | ||
| { | ||
| auto optional_commit = repo.resolve_local_ref(m_branch_name); | ||
| if (!optional_commit) | ||
| if (!pathspecs.empty()) | ||
| { | ||
| // TODO: handle remote refs | ||
| std::ostringstream buffer; | ||
| buffer << "error: could not resolve pathspec '" << m_branch_name << "'" << std::endl; | ||
| throw std::runtime_error(buffer.str()); | ||
| throw git_exception("error: '-b' or '-B' does not accept pathspecs.", git2cpp_error_code::BAD_ARGUMENT); | ||
| } | ||
|
|
||
| auto sl = status_list_wrapper::status_list(repo); | ||
| try | ||
| auto annotated_commit = create_local_branch(repo, target_name, m_force_create_flag); | ||
| checkout_tree(repo, annotated_commit, target_name, options); | ||
| update_head(repo, annotated_commit, target_name); | ||
|
|
||
| std::cout << "Switched to a new branch '" << target_name << "'" << std::endl; | ||
| return; | ||
| } | ||
|
|
||
| if (!pathspecs.empty()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer |
||
| { | ||
| // Try tree-ish + pathspec(s) | ||
| if (auto obj = repo.revparse_single(target_name)) | ||
| { | ||
| checkout_tree(repo, *optional_commit, m_branch_name, options); | ||
| update_head(repo, *optional_commit, m_branch_name); | ||
| // Validate all pathspecs before checkout so we can mimic git-like errors | ||
| for (const auto& p : pathspecs) | ||
| { | ||
| if (!std::filesystem::exists(std::filesystem::path(directory) / p) && !repo.does_track(p)) | ||
| { | ||
| throw git_exception( | ||
| "error: pathspec '" + p + "' did not match any file(s) known to git", | ||
| git2cpp_error_code::BAD_ARGUMENT | ||
| ); | ||
| } | ||
| } | ||
|
JohanMabille marked this conversation as resolved.
|
||
|
|
||
| options.checkout_strategy = GIT_CHECKOUT_FORCE; | ||
| checkout_paths(repo, target_name, pathspecs, options); | ||
| return; | ||
| } | ||
| catch (const git_exception& e) | ||
|
|
||
| // Else treat as files | ||
| for (const auto& p : pathspecs) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here too prefer an |
||
| { | ||
| if (sl.has_notstagged_header()) | ||
| if (!std::filesystem::exists(std::filesystem::path(directory) / p) && !repo.does_track(p)) | ||
| { | ||
| print_no_switch(sl); | ||
| throw git_exception( | ||
| "error: pathspec '" + p + "' did not match any file(s) known to git", | ||
| git2cpp_error_code::BAD_ARGUMENT | ||
| ); | ||
| } | ||
| throw e; | ||
| } | ||
|
|
||
| if (sl.has_notstagged_header()) | ||
| std::vector<std::string> files = m_positional_args; | ||
| options.checkout_strategy = GIT_CHECKOUT_FORCE; | ||
| checkout_files(repo, files, options); | ||
| return; | ||
| } | ||
|
|
||
| auto optional_commit = repo.resolve_local_ref(target_name); | ||
| if (!optional_commit) | ||
| { | ||
| // TODO: handle remote refs | ||
|
|
||
| // Fall back to checking out a unique file | ||
| const std::vector<std::string> file = {target_name}; | ||
|
|
||
| if (!std::filesystem::exists(std::filesystem::path(directory) / target_name)) | ||
| { | ||
| bool is_long = false; | ||
| bool is_coloured = false; | ||
| std::set<std::string> tracked_dir_set{}; | ||
| print_notstagged(sl, tracked_dir_set, is_long, is_coloured); | ||
| // Neither a branch/tag nor a file | ||
| throw git_exception( | ||
| "error: pathspec '" + target_name + "' did not match any file(s) known to git", | ||
| git2cpp_error_code::BAD_ARGUMENT | ||
| ); | ||
| } | ||
| if (sl.has_tobecommited_header()) | ||
|
|
||
| options.checkout_strategy = GIT_CHECKOUT_FORCE; | ||
| checkout_files(repo, file, options); | ||
| return; | ||
| } | ||
|
|
||
| auto sl = status_list_wrapper::status_list(repo); | ||
| try | ||
| { | ||
| checkout_tree(repo, *optional_commit, target_name, options); | ||
| update_head(repo, *optional_commit, target_name); | ||
| } | ||
| catch (const git_exception& e) | ||
| { | ||
| if (sl.has_notstagged_header()) | ||
| { | ||
| bool is_long = false; | ||
| bool is_coloured = false; | ||
| std::set<std::string> tracked_dir_set{}; | ||
| print_tobecommited(sl, tracked_dir_set, is_long, is_coloured); | ||
| print_no_switch(sl); | ||
| } | ||
| std::cout << "Switched to branch '" << m_branch_name << "'" << std::endl; | ||
| throw e; | ||
| } | ||
|
|
||
| if (sl.has_notstagged_header()) | ||
| { | ||
| bool is_long = false; | ||
| bool is_coloured = false; | ||
| std::set<std::string> tracked_dir_set{}; | ||
| print_notstagged(sl, tracked_dir_set, is_long, is_coloured); | ||
| } | ||
| if (sl.has_tobecommited_header()) | ||
| { | ||
| bool is_long = false; | ||
| bool is_coloured = false; | ||
| std::set<std::string> tracked_dir_set{}; | ||
| print_tobecommited(sl, tracked_dir_set, is_long, is_coloured); | ||
| } | ||
|
|
||
| std::string_view annotated_ref = optional_commit->reference_name(); | ||
| if (!annotated_ref.empty() && repo.find_reference(annotated_ref).is_branch()) | ||
| { | ||
| std::cout << "Switched to branch '" << target_name << "'" << std::endl; | ||
| print_tracking_info(repo, sl, true, false); | ||
| } | ||
| else | ||
| { | ||
| std::string sha = optional_commit->commit_oid_tostr().substr(0, 7); | ||
| auto commit = repo.find_commit(optional_commit->oid()); | ||
| std::string summary = commit.summary(); | ||
| std::cout << "HEAD is now at " << sha << " " << summary << std::endl; | ||
| } | ||
| } | ||
|
|
||
| annotated_commit_wrapper | ||
|
|
@@ -150,22 +282,71 @@ void checkout_subcommand::update_head( | |
| const std::string_view target_name | ||
| ) | ||
| { | ||
| // Check if HEAD is already detached or not | ||
| const bool head_was_detached = [&]() | ||
| { | ||
| auto head_ref = repo.head(); | ||
| return !head_ref.is_branch(); | ||
| }(); | ||
|
|
||
| // Save previous HEAD info (if it was detached) before changing it (for output message) | ||
| std::optional<commit_wrapper> previous_head_commit; | ||
| std::string previous_head_message; | ||
| if (head_was_detached) | ||
| { | ||
| previous_head_commit = repo.find_commit("HEAD"); | ||
| previous_head_message = "Previous HEAD position was " | ||
| + std::string(previous_head_commit.value().commit_oid_tostr().substr(0, 7)) | ||
| + " " + previous_head_commit.value().summary(); | ||
| } | ||
|
|
||
| std::string_view annotated_ref = target_annotated_commit.reference_name(); | ||
| if (!annotated_ref.empty()) | ||
| { | ||
| auto ref = repo.find_reference(annotated_ref); | ||
| if (ref.is_remote()) | ||
| if (ref.is_branch()) | ||
| { | ||
| auto branch = repo.create_branch(target_name, target_annotated_commit); | ||
| repo.set_head(branch.reference_name()); | ||
| if (head_was_detached) | ||
| { | ||
| std::cout << previous_head_message << std::endl; | ||
| } | ||
| repo.set_head(annotated_ref); | ||
| return; | ||
| } | ||
| else | ||
| } | ||
|
|
||
| repo.set_head_detached(target_annotated_commit); | ||
|
|
||
| if (head_was_detached) | ||
| { | ||
| // Only print "Previous HEAD position..." if HEAD was already detached before and if there is an | ||
| // actual checkout | ||
| auto new_head_commit = repo.find_commit("HEAD"); | ||
| if (!git_oid_equal(&previous_head_commit.value().oid(), &new_head_commit.oid())) | ||
| { | ||
| repo.set_head(annotated_ref); | ||
| std::cout << previous_head_message << std::endl; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| repo.set_head_detached(target_annotated_commit); | ||
| // Only print the detached-HEAD advice if HEAD was not already detached. | ||
| std::cout << "Note: switching to '" << target_name << "'." << std::endl; | ||
| std::cout << std::endl; | ||
| std::cout << "You are in 'detached HEAD' state. You can look around, make experimental" << std::endl; | ||
| std::cout << "changes and commit them, and you can discard any commits you make in this" << std::endl; | ||
| std::cout << "state without impacting any branches by switching back to a branch." << std::endl; | ||
| std::cout << std::endl; | ||
|
|
||
| // TODO: add to the following when the switch subcommand is implemented: | ||
| // std::cout << "If you want to create a new branch to retain commits you create, you may" << | ||
| // std::endl; std::cout << "do so (now or later) by using -c with the switch command. Example:" << | ||
| // std::endl; std::cout << " git switch -c <new-branch-name>" << std::endl; std::cout << std::endl; | ||
| // std::cout << "Or undo this operation with:" << std::endl; | ||
| // std::cout << std::endl; | ||
| // std::cout << " git switch -" << std::endl; | ||
| // std::cout << std::endl; | ||
| // TODO: add the following later | ||
| // std::cout << "Turn off this advice by setting config variable advice.detachedHead to false" | ||
| // << std::endl; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be renamed
checkout_head_files