Skip to content

Commit 29071fc

Browse files
committed
Implemented mv subcommand
1 parent 1e1c6b6 commit 29071fc

File tree

7 files changed

+330
-0
lines changed

7 files changed

+330
-0
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ set(GIT2CPP_SRC
6060
${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.hpp
6161
${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.cpp
6262
${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.hpp
63+
${GIT2CPP_SOURCE_DIR}/subcommand/mv_subcommand.cpp
64+
${GIT2CPP_SOURCE_DIR}/subcommand/mv_subcommand.hpp
6365
${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.cpp
6466
${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.hpp
6567
${GIT2CPP_SOURCE_DIR}/subcommand/rebase_subcommand.cpp

src/main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "subcommand/init_subcommand.hpp"
1616
#include "subcommand/log_subcommand.hpp"
1717
#include "subcommand/merge_subcommand.hpp"
18+
#include "subcommand/mv_subcommand.hpp"
1819
#include "subcommand/push_subcommand.hpp"
1920
#include "subcommand/rebase_subcommand.hpp"
2021
#include "subcommand/remote_subcommand.hpp"
@@ -48,6 +49,7 @@ int main(int argc, char** argv)
4849
reset_subcommand reset(lg2_obj, app);
4950
log_subcommand log(lg2_obj, app);
5051
merge_subcommand merge(lg2_obj, app);
52+
mv_subcommand mv(lg2_obj, app);
5153
push_subcommand push(lg2_obj, app);
5254
rebase_subcommand rebase(lg2_obj, app);
5355
remote_subcommand remote(lg2_obj, app);

src/subcommand/mv_subcommand.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include <filesystem>
2+
#include <system_error>
3+
#include "mv_subcommand.hpp"
4+
5+
#include "../utils/git_exception.hpp"
6+
#include "../wrapper/index_wrapper.hpp"
7+
#include "../wrapper/repository_wrapper.hpp"
8+
9+
namespace fs = std::filesystem;
10+
11+
mv_subcommand::mv_subcommand(const libgit2_object&, CLI::App& app)
12+
{
13+
auto* sub = app.add_subcommand("mv" , "Move or rename a file, a directory, or a symlink");
14+
sub->add_option("<source>", m_source_path, "The path of the source to move")->required()->check(CLI::ExistingFile);
15+
sub->add_option("<destination>", m_destination_path, "The path of the destination")->required();
16+
sub->add_flag("-f,--force", m_force, "Force renaming or moving of a file even if the <destination> exists.");
17+
18+
sub->callback([this]() { this->run(); });
19+
}
20+
21+
void mv_subcommand::run()
22+
{
23+
auto directory = get_current_git_path();
24+
auto repo = repository_wrapper::open(directory);
25+
26+
bool exists = fs::exists(m_destination_path) && !fs::is_directory(m_destination_path);
27+
if (exists && !m_force)
28+
{
29+
// TODO: replace magic number with enum when diff command is merged
30+
throw git_exception("destination already exists", 128);
31+
}
32+
33+
std::error_code ec;
34+
fs::rename(m_source_path, m_destination_path, ec);
35+
36+
if(ec)
37+
{
38+
throw git_exception("Could not move file", ec.value());
39+
}
40+
41+
auto index = repo.make_index();
42+
index.remove_entry(m_source_path);
43+
index.add_entry(m_destination_path);
44+
index.write();
45+
}

src/subcommand/mv_subcommand.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
3+
#include <CLI/CLI.hpp>
4+
#include <string>
5+
6+
#include "../utils/common.hpp"
7+
8+
class mv_subcommand
9+
{
10+
public:
11+
12+
explicit mv_subcommand(const libgit2_object&, CLI::App& app);
13+
void run();
14+
15+
private:
16+
17+
std::string m_source_path;
18+
std::string m_destination_path;
19+
bool m_force = false;
20+
};
21+

src/wrapper/index_wrapper.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ index_wrapper index_wrapper::init(repository_wrapper& rw)
2121
return index;
2222
}
2323

24+
void index_wrapper::add_entry(const std::string& path)
25+
{
26+
throw_if_error(git_index_add_bypath(*this, path.c_str()));
27+
}
28+
2429
void index_wrapper::add_entries(std::vector<std::string> patterns)
2530
{
2631
add_impl(std::move(patterns));
@@ -37,6 +42,11 @@ void index_wrapper::add_impl(std::vector<std::string> patterns)
3742
throw_if_error(git_index_add_all(*this, array, 0, NULL, NULL));
3843
}
3944

45+
void index_wrapper::remove_entry(const std::string& path)
46+
{
47+
throw_if_error(git_index_remove_bypath(*this, path.c_str()));
48+
}
49+
4050
void index_wrapper::write()
4151
{
4252
throw_if_error(git_index_write(*this));

src/wrapper/index_wrapper.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ class index_wrapper : public wrapper_base<git_index>
2323
void write();
2424
git_oid write_tree();
2525

26+
void add_entry(const std::string& path);
2627
void add_entries(std::vector<std::string> patterns);
2728
void add_all();
2829

30+
void remove_entry(const std::string& path);
31+
2932
bool has_conflict() const;
3033
void output_conflicts();
3134
void conflict_cleanup();

test/test_mv.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import subprocess
2+
3+
import pytest
4+
5+
6+
def test_mv_basic(xtl_clone, git2cpp_path, tmp_path):
7+
"""Test basic mv operation to rename a file"""
8+
assert (tmp_path / "xtl").exists()
9+
xtl_path = tmp_path / "xtl"
10+
11+
# Create a test file
12+
test_file = xtl_path / "test_file.txt"
13+
test_file.write_text("test content")
14+
15+
# Add the file to git
16+
add_cmd = [git2cpp_path, "add", "test_file.txt"]
17+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
18+
assert p_add.returncode == 0
19+
20+
# Move/rename the file
21+
mv_cmd = [git2cpp_path, "mv", "test_file.txt", "renamed_file.txt"]
22+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
23+
assert p_mv.returncode == 0
24+
25+
# Verify the file was moved
26+
assert not test_file.exists()
27+
assert (xtl_path / "renamed_file.txt").exists()
28+
29+
# Check git status
30+
status_cmd = [git2cpp_path, "status", "--long"]
31+
p_status = subprocess.run(status_cmd, capture_output=True, cwd=xtl_path, text=True)
32+
assert p_status.returncode == 0
33+
assert "renamed:" in p_status.stdout or "renamed_file.txt" in p_status.stdout
34+
35+
36+
def test_mv_to_subdirectory(xtl_clone, git2cpp_path, tmp_path):
37+
"""Test moving a file to a subdirectory"""
38+
assert (tmp_path / "xtl").exists()
39+
xtl_path = tmp_path / "xtl"
40+
41+
# Create a test file
42+
test_file = xtl_path / "move_me.txt"
43+
test_file.write_text("content to move")
44+
45+
# Add the file to git
46+
add_cmd = [git2cpp_path, "add", "move_me.txt"]
47+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
48+
assert p_add.returncode == 0
49+
50+
# Move the file to existing subdirectory
51+
mv_cmd = [git2cpp_path, "mv", "move_me.txt", "include/move_me.txt"]
52+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
53+
assert p_mv.returncode == 0
54+
55+
# Verify the file was moved
56+
assert not test_file.exists()
57+
assert (xtl_path / "include" / "move_me.txt").exists()
58+
59+
# Check git status
60+
status_cmd = [git2cpp_path, "status", "--long"]
61+
p_status = subprocess.run(status_cmd, capture_output=True, cwd=xtl_path, text=True)
62+
assert p_status.returncode == 0
63+
64+
65+
def test_mv_destination_exists_without_force(xtl_clone, git2cpp_path, tmp_path):
66+
"""Test that mv fails when destination exists without --force flag"""
67+
assert (tmp_path / "xtl").exists()
68+
xtl_path = tmp_path / "xtl"
69+
70+
# Create source file
71+
source_file = xtl_path / "source.txt"
72+
source_file.write_text("source content")
73+
74+
# Create destination file
75+
dest_file = xtl_path / "destination.txt"
76+
dest_file.write_text("destination content")
77+
78+
# Add both files to git
79+
add_cmd = [git2cpp_path, "add", "source.txt", "destination.txt"]
80+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
81+
assert p_add.returncode == 0
82+
83+
# Try to move without force - should fail
84+
mv_cmd = [git2cpp_path, "mv", "source.txt", "destination.txt"]
85+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
86+
assert p_mv.returncode != 0
87+
assert "destination already exists" in p_mv.stderr
88+
89+
# Verify source file still exists
90+
assert source_file.exists()
91+
assert dest_file.exists()
92+
93+
94+
@pytest.mark.parametrize("force_flag", ["-f", "--force"])
95+
def test_mv_destination_exists_with_force(xtl_clone, git2cpp_path, tmp_path, force_flag):
96+
"""Test that mv succeeds when destination exists with --force flag"""
97+
assert (tmp_path / "xtl").exists()
98+
xtl_path = tmp_path / "xtl"
99+
100+
# Create source file
101+
source_file = xtl_path / "source.txt"
102+
source_file.write_text("source content")
103+
104+
# Create destination file
105+
dest_file = xtl_path / "destination.txt"
106+
dest_file.write_text("destination content")
107+
108+
# Add both files to git
109+
add_cmd = [git2cpp_path, "add", "source.txt", "destination.txt"]
110+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
111+
assert p_add.returncode == 0
112+
113+
# Move with force - should succeed
114+
mv_cmd = [git2cpp_path, "mv", force_flag, "source.txt", "destination.txt"]
115+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
116+
assert p_mv.returncode == 0
117+
118+
# Verify source file was moved
119+
assert not source_file.exists()
120+
assert dest_file.exists()
121+
assert dest_file.read_text() == "source content"
122+
123+
124+
def test_mv_nonexistent_source(xtl_clone, git2cpp_path, tmp_path):
125+
"""Test that mv fails when source file doesn't exist"""
126+
assert (tmp_path / "xtl").exists()
127+
xtl_path = tmp_path / "xtl"
128+
129+
# Try to move a file that doesn't exist
130+
mv_cmd = [git2cpp_path, "mv", "nonexistent.txt", "destination.txt"]
131+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
132+
assert p_mv.returncode != 0
133+
134+
135+
def test_mv_multiple_files(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
136+
"""Test moving multiple files sequentially"""
137+
assert (tmp_path / "xtl").exists()
138+
xtl_path = tmp_path / "xtl"
139+
140+
# Create test files
141+
file1 = xtl_path / "file1.txt"
142+
file1.write_text("content 1")
143+
file2 = xtl_path / "file2.txt"
144+
file2.write_text("content 2")
145+
146+
# Add files to git
147+
add_cmd = [git2cpp_path, "add", "file1.txt", "file2.txt"]
148+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
149+
assert p_add.returncode == 0
150+
151+
# Commit the files
152+
commit_cmd = [git2cpp_path, "commit", "-m", "Add test files"]
153+
p_commit = subprocess.run(commit_cmd, capture_output=True, cwd=xtl_path, text=True)
154+
assert p_commit.returncode == 0
155+
156+
# Move first file
157+
mv_cmd1 = [git2cpp_path, "mv", "file1.txt", "renamed1.txt"]
158+
p_mv1 = subprocess.run(mv_cmd1, capture_output=True, cwd=xtl_path, text=True)
159+
assert p_mv1.returncode == 0
160+
161+
# Move second file
162+
mv_cmd2 = [git2cpp_path, "mv", "file2.txt", "renamed2.txt"]
163+
p_mv2 = subprocess.run(mv_cmd2, capture_output=True, cwd=xtl_path, text=True)
164+
assert p_mv2.returncode == 0
165+
166+
# Verify both files were moved
167+
assert not file1.exists()
168+
assert not file2.exists()
169+
assert (xtl_path / "renamed1.txt").exists()
170+
assert (xtl_path / "renamed2.txt").exists()
171+
172+
173+
def test_mv_and_commit(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
174+
"""Test moving a file and committing the change"""
175+
assert (tmp_path / "xtl").exists()
176+
xtl_path = tmp_path / "xtl"
177+
178+
# Create a test file
179+
test_file = xtl_path / "original.txt"
180+
test_file.write_text("original content")
181+
182+
# Add and commit the file
183+
add_cmd = [git2cpp_path, "add", "original.txt"]
184+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
185+
assert p_add.returncode == 0
186+
187+
commit_cmd = [git2cpp_path, "commit", "-m", "Add original file"]
188+
p_commit = subprocess.run(commit_cmd, capture_output=True, cwd=xtl_path, text=True)
189+
assert p_commit.returncode == 0
190+
191+
# Move the file
192+
mv_cmd = [git2cpp_path, "mv", "original.txt", "moved.txt"]
193+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
194+
assert p_mv.returncode == 0
195+
196+
# Check status before commit
197+
status_cmd = [git2cpp_path, "status", "--long"]
198+
p_status = subprocess.run(status_cmd, capture_output=True, cwd=xtl_path, text=True)
199+
assert p_status.returncode == 0
200+
assert "Changes to be committed" in p_status.stdout
201+
202+
# Commit the move
203+
commit_cmd2 = [git2cpp_path, "commit", "-m", "Move file"]
204+
p_commit2 = subprocess.run(commit_cmd2, capture_output=True, cwd=xtl_path, text=True)
205+
assert p_commit2.returncode == 0
206+
207+
# Verify the file is in the new location
208+
assert not (xtl_path / "original.txt").exists()
209+
assert (xtl_path / "moved.txt").exists()
210+
211+
212+
def test_mv_nogit(git2cpp_path, tmp_path):
213+
"""Test that mv fails when not in a git repository"""
214+
# Create a test file outside a git repo
215+
test_file = tmp_path / "test.txt"
216+
test_file.write_text("test content")
217+
218+
# Try to mv without being in a git repo
219+
mv_cmd = [git2cpp_path, "mv", "test.txt", "moved.txt"]
220+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=tmp_path, text=True)
221+
assert p_mv.returncode != 0
222+
223+
224+
def test_mv_preserve_content(xtl_clone, git2cpp_path, tmp_path):
225+
"""Test that file content is preserved after mv"""
226+
assert (tmp_path / "xtl").exists()
227+
xtl_path = tmp_path / "xtl"
228+
229+
# Create a test file with specific content
230+
test_content = "This is important content that should be preserved"
231+
test_file = xtl_path / "important.txt"
232+
test_file.write_text(test_content)
233+
234+
# Add the file to git
235+
add_cmd = [git2cpp_path, "add", "important.txt"]
236+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
237+
assert p_add.returncode == 0
238+
239+
# Move the file
240+
mv_cmd = [git2cpp_path, "mv", "important.txt", "preserved.txt"]
241+
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
242+
assert p_mv.returncode == 0
243+
244+
# Verify content is preserved
245+
moved_file = xtl_path / "preserved.txt"
246+
assert moved_file.exists()
247+
assert moved_file.read_text() == test_content

0 commit comments

Comments
 (0)