Skip to content

Commit 42d1072

Browse files
authored
Implemented mv subcommand (#93)
* Implemented mv subcommand * Applied review comments
1 parent e00fe2e commit 42d1072

File tree

8 files changed

+334
-0
lines changed

8 files changed

+334
-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/conftest_wasm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def pytest_ignore_collect(collection_path: pathlib.Path) -> bool:
2424
"test_init.py",
2525
"test_log.py",
2626
"test_merge.py",
27+
"test_mv.py",
2728
"test_rebase.py",
2829
"test_remote.py",
2930
"test_reset.py",

test/test_mv.py

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

0 commit comments

Comments
 (0)