Skip to content

Commit 289f069

Browse files
authored
Add tag subcommand (#96)
* Add tag subcommand * address review comments
1 parent 7fad05e commit 289f069

18 files changed

+675
-13
lines changed

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ set(GIT2CPP_SRC
8282
${GIT2CPP_SOURCE_DIR}/subcommand/stash_subcommand.hpp
8383
${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp
8484
${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.hpp
85+
${GIT2CPP_SOURCE_DIR}/subcommand/tag_subcommand.cpp
86+
${GIT2CPP_SOURCE_DIR}/subcommand/tag_subcommand.hpp
8587
${GIT2CPP_SOURCE_DIR}/utils/ansi_code.cpp
8688
${GIT2CPP_SOURCE_DIR}/utils/ansi_code.hpp
8789
${GIT2CPP_SOURCE_DIR}/utils/common.cpp
@@ -126,6 +128,8 @@ set(GIT2CPP_SRC
126128
${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.hpp
127129
${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.cpp
128130
${GIT2CPP_SOURCE_DIR}/wrapper/status_wrapper.hpp
131+
${GIT2CPP_SOURCE_DIR}/wrapper/tag_wrapper.cpp
132+
${GIT2CPP_SOURCE_DIR}/wrapper/tag_wrapper.hpp
129133
${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.cpp
130134
${GIT2CPP_SOURCE_DIR}/wrapper/tree_wrapper.hpp
131135
${GIT2CPP_SOURCE_DIR}/wrapper/wrapper_base.hpp

src/main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "subcommand/reset_subcommand.hpp"
2424
#include "subcommand/stash_subcommand.hpp"
2525
#include "subcommand/status_subcommand.hpp"
26+
#include "subcommand/tag_subcommand.hpp"
2627
#include "subcommand/revparse_subcommand.hpp"
2728
#include "subcommand/revlist_subcommand.hpp"
2829
#include "subcommand/rm_subcommand.hpp"
@@ -60,6 +61,7 @@ int main(int argc, char** argv)
6061
revparse_subcommand revparse(lg2_obj, app);
6162
rm_subcommand rm(lg2_obj, app);
6263
stash_subcommand stash(lg2_obj, app);
64+
tag_subcommand tag(lg2_obj, app);
6365

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

src/subcommand/log_subcommand.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ void print_commit(const commit_wrapper& commit, std::string m_format_flag)
7878
print_time(author.when(), "Date:\t");
7979
}
8080
}
81-
std::cout << "\n " << git_commit_message(commit) << "\n" << std::endl;
81+
std::cout << "\n " << commit.message() << "\n" << std::endl;
8282
}
8383

8484
void log_subcommand::run()

src/subcommand/tag_subcommand.cpp

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
#include <git2.h>
2+
3+
#include "../subcommand/tag_subcommand.hpp"
4+
#include "../wrapper/commit_wrapper.hpp"
5+
#include "../wrapper/tag_wrapper.hpp"
6+
7+
tag_subcommand::tag_subcommand(const libgit2_object&, CLI::App& app)
8+
{
9+
auto* sub = app.add_subcommand("tag", "Create, list, delete or verify tags");
10+
11+
sub->add_flag("-l,--list", m_list_flag, "List tags. With optional <pattern>.");
12+
sub->add_flag("-f,--force", m_force_flag, "Replace an existing tag with the given name (instead of failing)");
13+
sub->add_option("-d,--delete", m_delete, "Delete existing tags with the given names.");
14+
sub->add_option("-n", m_num_lines, "<num> specifies how many lines from the annotation, if any, are printed when using -l. Implies --list.");
15+
sub->add_option("-m,--message", m_message, "Tag message for annotated tags");
16+
sub->add_option("<tagname>", m_tag_name, "Tag name");
17+
sub->add_option("<commit>", m_target, "Target commit (defaults to HEAD)");
18+
19+
sub->callback([this]() { this->run(); });
20+
}
21+
22+
// Tag listing: Print individual message lines
23+
void print_list_lines(const std::string& message, int num_lines)
24+
{
25+
if (message.empty())
26+
{
27+
return;
28+
}
29+
30+
auto lines = split_input_at_newlines(message);
31+
32+
// header
33+
std::cout << lines[0];
34+
35+
// other lines
36+
if (num_lines <= 1 || lines.size() <= 2)
37+
{
38+
std::cout << std::endl;
39+
}
40+
else
41+
{
42+
for (size_t i = 1; i < lines.size() ; i++)
43+
{
44+
if (i < num_lines)
45+
{
46+
std::cout << "\n\t\t" << lines[i];
47+
}
48+
}
49+
}
50+
}
51+
52+
// Tag listing: Print an actual tag object
53+
void print_tag(git_tag* tag, int num_lines)
54+
{
55+
std::cout << std::left << std::setw(16) << git_tag_name(tag);
56+
57+
if (num_lines)
58+
{
59+
std::string msg = git_tag_message(tag);
60+
if (!msg.empty())
61+
{
62+
print_list_lines(msg, num_lines);
63+
}
64+
else
65+
{
66+
std::cout << std::endl;
67+
}
68+
}
69+
else
70+
{
71+
std::cout << std::endl;
72+
}
73+
}
74+
75+
// Tag listing: Print a commit (target of a lightweight tag)
76+
void print_commit(git_commit* commit, std::string name, int num_lines)
77+
{
78+
std::cout << std::left << std::setw(16) << name;
79+
80+
if (num_lines)
81+
{
82+
std::string msg = git_commit_message(commit);
83+
if (!msg.empty())
84+
{
85+
print_list_lines(msg, num_lines);
86+
}
87+
else
88+
{
89+
std::cout <<std::endl;
90+
}
91+
}
92+
else
93+
{
94+
std::cout <<std::endl;
95+
}
96+
}
97+
98+
// Tag listing: Lookup tags based on ref name and dispatch to print
99+
void each_tag(repository_wrapper& repo, const std::string& name, int num_lines)
100+
{
101+
auto obj = repo.revparse_single(name);
102+
103+
if (obj.has_value())
104+
{
105+
switch (git_object_type(obj.value()))
106+
{
107+
case GIT_OBJECT_TAG:
108+
print_tag(obj.value(), num_lines);
109+
break;
110+
case GIT_OBJECT_COMMIT:
111+
print_commit(obj.value(), name, num_lines);
112+
break;
113+
default:
114+
std::cout << name << std::endl;
115+
}
116+
}
117+
else
118+
{
119+
std::cout << name << std::endl;
120+
}
121+
}
122+
123+
void tag_subcommand::list_tags(repository_wrapper& repo)
124+
{
125+
std::string pattern = m_tag_name.empty() ? "*" : m_tag_name;
126+
auto tag_names = repo.tag_list_match(pattern);
127+
128+
for (const auto& tag_name: tag_names)
129+
{
130+
each_tag(repo, tag_name, m_num_lines);
131+
}
132+
}
133+
134+
void tag_subcommand::delete_tag(repository_wrapper& repo)
135+
{
136+
if (m_delete.empty())
137+
{
138+
throw git_exception("Name required for tag deletion.", git2cpp_error_code::GENERIC_ERROR);
139+
}
140+
141+
auto obj = repo.revparse_single(m_delete);
142+
if (!obj.has_value())
143+
{
144+
throw git_exception("error: tag '" + m_delete + "' not found.", git2cpp_error_code::GENERIC_ERROR);
145+
}
146+
147+
git_buf abbrev_oid = GIT_BUF_INIT;
148+
throw_if_error(git_object_short_id(&abbrev_oid, obj.value()));
149+
150+
std::string oid_str(abbrev_oid.ptr);
151+
git_buf_dispose(&abbrev_oid);
152+
153+
throw_if_error(git_tag_delete(repo, m_delete.c_str()));
154+
std::cout << "Deleted tag '" << m_delete << "' (was " << oid_str << ")" << std::endl;
155+
}
156+
157+
std::optional<object_wrapper> tag_subcommand::get_target_obj(repository_wrapper& repo)
158+
{
159+
if (m_tag_name.empty())
160+
{
161+
throw git_exception("Tag name required", git2cpp_error_code::GENERIC_ERROR);
162+
}
163+
164+
std::string target = m_target.empty() ? "HEAD" : m_target;
165+
166+
auto target_obj = repo.revparse_single(target);
167+
if (!target_obj.has_value())
168+
{
169+
throw git_exception("Unable to resolve target: " + target, git2cpp_error_code::GENERIC_ERROR);
170+
}
171+
172+
return target_obj;
173+
}
174+
175+
void tag_subcommand::handle_error(int error)
176+
{
177+
if (error < 0)
178+
{
179+
if (error == GIT_EEXISTS)
180+
{
181+
throw git_exception("tag '" + m_tag_name + "' already exists", git2cpp_error_code::FILESYSTEM_ERROR);
182+
}
183+
throw git_exception("Unable to create annotated tag", error);
184+
}
185+
}
186+
187+
void tag_subcommand::create_lightweight_tag(repository_wrapper& repo)
188+
{
189+
auto target_obj = tag_subcommand::get_target_obj(repo);
190+
191+
git_oid oid;
192+
size_t force = m_force_flag ? 1 : 0;
193+
int error = git_tag_create_lightweight(&oid, repo, m_tag_name.c_str(), target_obj.value(), force);
194+
195+
handle_error(error);
196+
}
197+
198+
void tag_subcommand::create_tag(repository_wrapper& repo)
199+
{
200+
auto target_obj = tag_subcommand::get_target_obj(repo);
201+
202+
auto tagger = signature_wrapper::get_default_signature_from_env(repo);
203+
204+
git_oid oid;
205+
size_t force = m_force_flag ? 1 : 0;
206+
int error = git_tag_create(&oid, repo, m_tag_name.c_str(), target_obj.value(), tagger.first, m_message.c_str(), force);
207+
208+
handle_error(error);
209+
}
210+
211+
void tag_subcommand::run()
212+
{
213+
auto directory = get_current_git_path();
214+
auto repo = repository_wrapper::open(directory);
215+
216+
if (!m_delete.empty())
217+
{
218+
delete_tag(repo);
219+
}
220+
else if (m_list_flag || (m_tag_name.empty() && m_message.empty()))
221+
{
222+
list_tags(repo);
223+
}
224+
else if (!m_message.empty())
225+
{
226+
create_tag(repo);
227+
}
228+
else if (!m_tag_name.empty())
229+
{
230+
create_lightweight_tag(repo);
231+
}
232+
else
233+
{
234+
list_tags(repo);
235+
}
236+
237+
}

src/subcommand/tag_subcommand.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#pragma once
2+
3+
#include <CLI/CLI.hpp>
4+
5+
#include "../utils/common.hpp"
6+
#include "../wrapper/repository_wrapper.hpp"
7+
8+
class tag_subcommand
9+
{
10+
public:
11+
12+
explicit tag_subcommand(const libgit2_object&, CLI::App& app);
13+
14+
void run();
15+
16+
private:
17+
18+
void list_tags(repository_wrapper& repo);
19+
void delete_tag(repository_wrapper& repo);
20+
void create_lightweight_tag(repository_wrapper& repo);
21+
void create_tag(repository_wrapper& repo);
22+
std::optional<object_wrapper> get_target_obj(repository_wrapper& repo);
23+
void handle_error(int error);
24+
25+
std::string m_delete;
26+
std::string m_message;
27+
std::string m_tag_name;
28+
std::string m_target;
29+
bool m_list_flag = false;
30+
bool m_force_flag = false;
31+
int m_num_lines = 0;
32+
};

src/utils/common.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include <sstream>
55
#include <unistd.h>
66
#include <map>
7+
#include <ranges>
8+
9+
#include <git2.h>
710

811
#include "common.hpp"
912
#include "git_exception.hpp"
@@ -103,6 +106,11 @@ void git_strarray_wrapper::init_str_array()
103106
}
104107
}
105108

109+
size_t git_strarray_wrapper::size()
110+
{
111+
return m_patterns.size();
112+
}
113+
106114
std::string read_file(const std::string& path)
107115
{
108116
std::ifstream file(path, std::ios::binary);
@@ -114,3 +122,12 @@ std::string read_file(const std::string& path)
114122
buffer << file.rdbuf();
115123
return buffer.str();
116124
}
125+
126+
std::vector<std::string> split_input_at_newlines(std::string_view str)
127+
{
128+
auto split = str | std::ranges::views::split('\n')
129+
| std::ranges::views::transform([](auto&& range) {
130+
return std::string(range.begin(), range.end());
131+
});
132+
return std::vector<std::string>{split.begin(), split.end()};
133+
}

src/utils/common.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class git_strarray_wrapper
5757

5858
operator git_strarray*();
5959

60+
size_t size();
61+
6062
private:
6163
std::vector<std::string> m_patterns;
6264
git_strarray m_array;
@@ -66,3 +68,5 @@ class git_strarray_wrapper
6668
};
6769

6870
std::string read_file(const std::string& path);
71+
72+
std::vector<std::string> split_input_at_newlines(std::string_view str);

src/utils/terminal_pager.cpp

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "ansi_code.hpp"
1313
#include "output.hpp"
1414
#include "terminal_pager.hpp"
15+
#include "common.hpp"
1516

1617
terminal_pager::terminal_pager()
1718
: m_rows(0), m_columns(0), m_start_row_index(0)
@@ -167,7 +168,7 @@ void terminal_pager::show()
167168
{
168169
release_cout();
169170

170-
split_input_at_newlines(m_stringbuf.view());
171+
m_lines = split_input_at_newlines(m_stringbuf.view());
171172

172173
update_terminal_size();
173174
if (m_rows == 0 || m_lines.size() <= m_rows - 1)
@@ -196,15 +197,6 @@ void terminal_pager::show()
196197
m_start_row_index = 0;
197198
}
198199

199-
void terminal_pager::split_input_at_newlines(std::string_view str)
200-
{
201-
auto split = str | std::ranges::views::split('\n')
202-
| std::ranges::views::transform([](auto&& range) {
203-
return std::string(range.begin(), range.end());
204-
});
205-
m_lines = std::vector<std::string>{split.begin(), split.end()};
206-
}
207-
208200
void terminal_pager::update_terminal_size()
209201
{
210202
struct winsize size;

0 commit comments

Comments
 (0)