Skip to content

Commit a16dc5e

Browse files
committed
Add 'checkout <tree-ish> <pathspecs>', fix 'chekout tag', fix output messages
1 parent 1136abe commit a16dc5e

7 files changed

Lines changed: 323 additions & 54 deletions

File tree

src/subcommand/checkout_subcommand.cpp

Lines changed: 188 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <iostream>
55
#include <set>
66

7+
#include <git2/oid.h>
8+
79
#include "../subcommand/status_subcommand.hpp"
810
#include "../utils/git_exception.hpp"
911
#include "../wrapper/repository_wrapper.hpp"
@@ -14,7 +16,9 @@ checkout_subcommand::checkout_subcommand(const libgit2_object&, CLI::App& app)
1416
auto* sub = app.add_subcommand("checkout", "Switch branches or restore working tree files");
1517

1618
// "-- file" lands in m_positional_args because CLI11 consumes "--" silently.
17-
sub->add_option("<branch|files>", m_positional_args, "Branch to checkout, or one/many file path(s)");
19+
sub->add_option("<tree-ish|pathspec>", m_positional_args, "Tree-ish to checkout, and/or one/many pathspec(s)");
20+
// checkout <branch>, checkout <tag>, checkout <file> ..., checkout <branch> <file> ...
21+
// Use without "--"
1822
sub->add_flag("-b", m_create_flag, "Create a new branch before checking it out");
1923
sub->add_flag("-B", m_force_create_flag, "Create a new branch or reset it if it exists before checking it out");
2024
sub->add_flag(
@@ -72,6 +76,36 @@ void checkout_subcommand::checkout_files(
7276
throw_if_error(git_checkout_head(repo, &options));
7377
}
7478

79+
void checkout_subcommand::checkout_paths(
80+
const repository_wrapper& repo,
81+
const std::string_view tree_ish,
82+
const std::vector<std::string>& pathspecs,
83+
const git_checkout_options& base_options
84+
)
85+
{
86+
auto obj = repo.revparse_single(tree_ish);
87+
if (!obj)
88+
{
89+
throw git_exception(
90+
"error: could not resolve tree-ish '" + std::string(tree_ish) + "'",
91+
git2cpp_error_code::BAD_ARGUMENT
92+
);
93+
}
94+
95+
std::vector<const char*> pathspec_strings;
96+
pathspec_strings.reserve(pathspecs.size());
97+
for (const auto& p : pathspecs)
98+
{
99+
pathspec_strings.push_back(p.c_str());
100+
}
101+
102+
git_checkout_options options = base_options;
103+
options.paths.strings = const_cast<char**>(pathspec_strings.data());
104+
options.paths.count = pathspec_strings.size();
105+
106+
throw_if_error(git_checkout_tree(repo, *obj, &options));
107+
}
108+
75109
void checkout_subcommand::run()
76110
{
77111
auto directory = get_current_git_path();
@@ -99,77 +133,129 @@ void checkout_subcommand::run()
99133
throw std::runtime_error("error: no branch or file specified");
100134
}
101135

102-
std::string branch_name = m_positional_args[0];
136+
const std::string& target_name = m_positional_args[0]; // can be a branch or a tag
137+
const std::vector<std::string> pathspecs(m_positional_args.begin() + 1, m_positional_args.end());
138+
103139
if (m_create_flag || m_force_create_flag)
104140
{
105-
auto annotated_commit = create_local_branch(repo, branch_name, m_force_create_flag);
106-
checkout_tree(repo, annotated_commit, branch_name, options);
107-
update_head(repo, annotated_commit, branch_name);
141+
if (!pathspecs.empty())
142+
{
143+
throw git_exception("error: '-b' or '-B' does not accept pathspecs.", git2cpp_error_code::BAD_ARGUMENT);
144+
}
145+
146+
auto annotated_commit = create_local_branch(repo, target_name, m_force_create_flag);
147+
checkout_tree(repo, annotated_commit, target_name, options);
148+
update_head(repo, annotated_commit, target_name);
108149

109-
std::cout << "Switched to a new branch '" << branch_name << "'" << std::endl;
150+
std::cout << "Switched to a new branch '" << target_name << "'" << std::endl;
151+
return;
110152
}
111-
else
153+
154+
if (!pathspecs.empty())
112155
{
113-
auto optional_commit = repo.resolve_local_ref(branch_name);
114-
if (!optional_commit)
156+
// Try tree-ish + pathspec(s)
157+
if (auto obj = repo.revparse_single(target_name))
115158
{
116-
// TODO: handle remote refs
117-
118-
// Fall back to file restore only if at least one path exists on disk.
119-
// If none do, it's an unresolvable branch name — report it as such.
120-
bool any_exists = std::any_of(
121-
m_positional_args.begin(),
122-
m_positional_args.end(),
123-
[&](const std::string& p)
159+
// Validate all pathspecs before checkout so we can mimic git-like errors
160+
for (const auto& p : pathspecs)
161+
{
162+
if (!std::filesystem::exists(std::filesystem::path(directory) / p) && !repo.does_track(p))
124163
{
125-
return std::filesystem::exists(std::filesystem::path(directory) / p);
164+
throw git_exception(
165+
"error: pathspec '" + p + "' did not match any file(s) known to git",
166+
git2cpp_error_code::BAD_ARGUMENT
167+
);
126168
}
127-
);
128-
129-
if (!any_exists)
130-
{
131-
std::ostringstream buffer;
132-
buffer << "error: could not resolve pathspec '" << branch_name << "'" << std::endl;
133-
throw std::runtime_error(buffer.str());
134169
}
135170

136171
options.checkout_strategy = GIT_CHECKOUT_FORCE;
137-
checkout_files(repo, m_positional_args, options);
172+
checkout_paths(repo, target_name, pathspecs, options);
138173
return;
139174
}
140175

141-
auto sl = status_list_wrapper::status_list(repo);
142-
try
176+
// Else treat as files
177+
for (const auto& p : pathspecs)
143178
{
144-
checkout_tree(repo, *optional_commit, branch_name, options);
145-
update_head(repo, *optional_commit, branch_name);
146-
}
147-
catch (const git_exception& e)
148-
{
149-
if (sl.has_notstagged_header())
179+
if (!std::filesystem::exists(std::filesystem::path(directory) / p) && !repo.does_track(p))
150180
{
151-
print_no_switch(sl);
181+
throw git_exception(
182+
"error: pathspec '" + p + "' did not match any file(s) known to git",
183+
git2cpp_error_code::BAD_ARGUMENT
184+
);
152185
}
153-
throw e;
154186
}
155187

156-
if (sl.has_notstagged_header())
188+
std::vector<std::string> files = m_positional_args;
189+
options.checkout_strategy = GIT_CHECKOUT_FORCE;
190+
checkout_files(repo, files, options);
191+
return;
192+
}
193+
194+
auto optional_commit = repo.resolve_local_ref(target_name);
195+
if (!optional_commit)
196+
{
197+
// TODO: handle remote refs
198+
199+
// Fall back to checking out a unique file
200+
const std::vector<std::string> file = {target_name};
201+
202+
if (!std::filesystem::exists(std::filesystem::path(directory) / target_name))
157203
{
158-
bool is_long = false;
159-
bool is_coloured = false;
160-
std::set<std::string> tracked_dir_set{};
161-
print_notstagged(sl, tracked_dir_set, is_long, is_coloured);
204+
// Neither a branch/tag nor a file
205+
throw git_exception(
206+
"error: pathspec '" + target_name + "' did not match any file(s) known to git",
207+
git2cpp_error_code::BAD_ARGUMENT
208+
);
162209
}
163-
if (sl.has_tobecommited_header())
210+
211+
options.checkout_strategy = GIT_CHECKOUT_FORCE;
212+
checkout_files(repo, file, options);
213+
return;
214+
}
215+
216+
auto sl = status_list_wrapper::status_list(repo);
217+
try
218+
{
219+
checkout_tree(repo, *optional_commit, target_name, options);
220+
update_head(repo, *optional_commit, target_name);
221+
}
222+
catch (const git_exception& e)
223+
{
224+
if (sl.has_notstagged_header())
164225
{
165-
bool is_long = false;
166-
bool is_coloured = false;
167-
std::set<std::string> tracked_dir_set{};
168-
print_tobecommited(sl, tracked_dir_set, is_long, is_coloured);
226+
print_no_switch(sl);
169227
}
170-
std::cout << "Switched to branch '" << branch_name << "'" << std::endl;
228+
throw e;
229+
}
230+
231+
if (sl.has_notstagged_header())
232+
{
233+
bool is_long = false;
234+
bool is_coloured = false;
235+
std::set<std::string> tracked_dir_set{};
236+
print_notstagged(sl, tracked_dir_set, is_long, is_coloured);
237+
}
238+
if (sl.has_tobecommited_header())
239+
{
240+
bool is_long = false;
241+
bool is_coloured = false;
242+
std::set<std::string> tracked_dir_set{};
243+
print_tobecommited(sl, tracked_dir_set, is_long, is_coloured);
244+
}
245+
246+
std::string_view annotated_ref = optional_commit->reference_name();
247+
if (!annotated_ref.empty() && repo.find_reference(annotated_ref).is_branch())
248+
{
249+
std::cout << "Switched to branch '" << target_name << "'" << std::endl;
171250
print_tracking_info(repo, sl, true, false);
172251
}
252+
else
253+
{
254+
std::string sha = optional_commit->commit_oid_tostr().substr(0, 7);
255+
auto commit = repo.find_commit(optional_commit->oid());
256+
std::string summary = commit.summary();
257+
std::cout << "HEAD is now at " << sha << " " << summary << std::endl;
258+
}
173259
}
174260

175261
annotated_commit_wrapper
@@ -196,22 +282,71 @@ void checkout_subcommand::update_head(
196282
const std::string_view target_name
197283
)
198284
{
285+
// Check if HEAD is already detached or not
286+
const bool head_was_detached = [&]()
287+
{
288+
auto head_ref = repo.head();
289+
return !head_ref.is_branch();
290+
}();
291+
292+
// Save previous HEAD info (if it was detached) before changing it (for output message)
293+
std::optional<commit_wrapper> previous_head_commit;
294+
std::string previous_head_message;
295+
if (head_was_detached)
296+
{
297+
previous_head_commit = repo.find_commit("HEAD");
298+
previous_head_message = "Previous HEAD position was "
299+
+ std::string(previous_head_commit.value().commit_oid_tostr().substr(0, 7)) + " "
300+
+ previous_head_commit.value().summary();
301+
}
302+
199303
std::string_view annotated_ref = target_annotated_commit.reference_name();
200304
if (!annotated_ref.empty())
201305
{
202306
auto ref = repo.find_reference(annotated_ref);
203-
if (ref.is_remote())
307+
if (ref.is_branch())
204308
{
205-
auto branch = repo.create_branch(target_name, target_annotated_commit);
206-
repo.set_head(branch.reference_name());
309+
if (head_was_detached)
310+
{
311+
std::cout << previous_head_message << std::endl;
312+
}
313+
repo.set_head(annotated_ref);
314+
return;
207315
}
208-
else
316+
}
317+
318+
repo.set_head_detached(target_annotated_commit);
319+
320+
if (head_was_detached)
321+
{
322+
// Only print "Previous HEAD position..." if HEAD was already detached before and if there is an
323+
// actual checkout
324+
auto new_head_commit = repo.find_commit("HEAD");
325+
if (!git_oid_equal(&previous_head_commit.value().oid(), &new_head_commit.oid()))
209326
{
210-
repo.set_head(annotated_ref);
327+
std::cout << previous_head_message << std::endl;
211328
}
212329
}
213330
else
214331
{
215-
repo.set_head_detached(target_annotated_commit);
332+
// Only print the detached-HEAD advice if HEAD was not already detached.
333+
std::cout << "Note: switching to '" << target_name << "'." << std::endl;
334+
std::cout << std::endl;
335+
std::cout << "You are in 'detached HEAD' state. You can look around, make experimental" << std::endl;
336+
std::cout << "changes and commit them, and you can discard any commits you make in this" << std::endl;
337+
std::cout << "state without impacting any branches by switching back to a branch." << std::endl;
338+
std::cout << std::endl;
339+
340+
// TODO: add to the following when the switch subcommand is implemented:
341+
// std::cout << "If you want to create a new branch to retain commits you create, you may" <<
342+
// std::endl; std::cout << "do so (now or later) by using -c with the switch command. Example:" <<
343+
// std::endl; std::cout << " git switch -c <new-branch-name>" << std::endl; std::cout << std::endl;
344+
// std::cout << "Or undo this operation with:" << std::endl;
345+
// std::cout << std::endl;
346+
// std::cout << " git switch -" << std::endl;
347+
// std::cout << std::endl;
348+
// TODO: add the following later
349+
// std::cout << "Turn off this advice by setting config variable advice.detachedHead to false"
350+
// << std::endl;
216351
}
217352
}

src/subcommand/checkout_subcommand.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class checkout_subcommand
3939
const git_checkout_options& options
4040
);
4141

42+
void checkout_paths(
43+
const repository_wrapper& repo,
44+
const std::string_view tree_ish,
45+
const std::vector<std::string>& pathspecs,
46+
const git_checkout_options& options
47+
);
48+
4249
std::vector<std::string> m_positional_args = {};
4350
bool m_create_flag = false;
4451
bool m_force_create_flag = false;

src/wrapper/annotated_commit_wrapper.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const git_oid& annotated_commit_wrapper::oid() const
1616
return *git_annotated_commit_id(p_resource);
1717
}
1818

19+
std::string annotated_commit_wrapper::commit_oid_tostr() const
20+
{
21+
char buf[GIT_OID_SHA1_HEXSIZE + 1];
22+
return git_oid_tostr(buf, sizeof(buf), &this->oid());
23+
}
24+
1925
std::string_view annotated_commit_wrapper::reference_name() const
2026
{
2127
const char* res = git_annotated_commit_ref(*this);

src/wrapper/annotated_commit_wrapper.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <string>
34
#include <string_view>
45

56
#include <git2.h>
@@ -18,6 +19,7 @@ class annotated_commit_wrapper : public wrapper_base<git_annotated_commit>
1819
annotated_commit_wrapper& operator=(annotated_commit_wrapper&&) noexcept = default;
1920

2021
const git_oid& oid() const;
22+
std::string commit_oid_tostr() const;
2123
std::string_view reference_name() const;
2224

2325
private:

src/wrapper/refs_wrapper.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ bool reference_wrapper::is_remote() const
2727
return git_reference_is_remote(*this);
2828
}
2929

30+
bool reference_wrapper::is_branch() const
31+
{
32+
return git_reference_is_branch(*this);
33+
}
34+
3035
const git_oid* reference_wrapper::target() const
3136
{
3237
return git_reference_target(p_resource);

src/wrapper/refs_wrapper.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class reference_wrapper : public wrapper_base<git_reference>
2222

2323
std::string short_name() const;
2424
bool is_remote() const;
25+
bool is_branch() const;
2526
const git_oid* target() const;
2627
reference_wrapper write_new_ref(const git_oid target_oid);
2728

0 commit comments

Comments
 (0)