Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ set(GIT2CPP_SRC
${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.cpp
${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/mv_subcommand.cpp
${GIT2CPP_SOURCE_DIR}/subcommand/mv_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.cpp
${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.hpp
${GIT2CPP_SOURCE_DIR}/subcommand/rebase_subcommand.cpp
Expand Down
2 changes: 2 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "subcommand/init_subcommand.hpp"
#include "subcommand/log_subcommand.hpp"
#include "subcommand/merge_subcommand.hpp"
#include "subcommand/mv_subcommand.hpp"
#include "subcommand/push_subcommand.hpp"
#include "subcommand/rebase_subcommand.hpp"
#include "subcommand/remote_subcommand.hpp"
Expand Down Expand Up @@ -48,6 +49,7 @@ int main(int argc, char** argv)
reset_subcommand reset(lg2_obj, app);
log_subcommand log(lg2_obj, app);
merge_subcommand merge(lg2_obj, app);
mv_subcommand mv(lg2_obj, app);
push_subcommand push(lg2_obj, app);
rebase_subcommand rebase(lg2_obj, app);
remote_subcommand remote(lg2_obj, app);
Expand Down
45 changes: 45 additions & 0 deletions src/subcommand/mv_subcommand.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <filesystem>
#include <system_error>
#include "mv_subcommand.hpp"

#include "../utils/git_exception.hpp"
#include "../wrapper/index_wrapper.hpp"
#include "../wrapper/repository_wrapper.hpp"

namespace fs = std::filesystem;

mv_subcommand::mv_subcommand(const libgit2_object&, CLI::App& app)
{
auto* sub = app.add_subcommand("mv" , "Move or rename a file, a directory, or a symlink");
sub->add_option("<source>", m_source_path, "The path of the source to move")->required()->check(CLI::ExistingFile);
sub->add_option("<destination>", m_destination_path, "The path of the destination")->required();
sub->add_flag("-f,--force", m_force, "Force renaming or moving of a file even if the <destination> exists.");

sub->callback([this]() { this->run(); });
}

void mv_subcommand::run()
{
auto directory = get_current_git_path();
auto repo = repository_wrapper::open(directory);

bool exists = fs::exists(m_destination_path) && !fs::is_directory(m_destination_path);
if (exists && !m_force)
{
// TODO: replace magic number with enum when diff command is merged
throw git_exception("destination already exists", 128);
}

std::error_code ec;
fs::rename(m_source_path, m_destination_path, ec);

if(ec)
{
throw git_exception("Could not move file", ec.value());
}

auto index = repo.make_index();
index.remove_entry(m_source_path);
index.add_entry(m_destination_path);
index.write();
}
21 changes: 21 additions & 0 deletions src/subcommand/mv_subcommand.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <CLI/CLI.hpp>
#include <string>

#include "../utils/common.hpp"

class mv_subcommand
{
public:

explicit mv_subcommand(const libgit2_object&, CLI::App& app);
void run();

private:

std::string m_source_path;
std::string m_destination_path;
bool m_force = false;
};

10 changes: 10 additions & 0 deletions src/wrapper/index_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ index_wrapper index_wrapper::init(repository_wrapper& rw)
return index;
}

void index_wrapper::add_entry(const std::string& path)
{
throw_if_error(git_index_add_bypath(*this, path.c_str()));
}

void index_wrapper::add_entries(std::vector<std::string> patterns)
{
add_impl(std::move(patterns));
Expand All @@ -37,6 +42,11 @@ void index_wrapper::add_impl(std::vector<std::string> patterns)
throw_if_error(git_index_add_all(*this, array, 0, NULL, NULL));
}

void index_wrapper::remove_entry(const std::string& path)
{
throw_if_error(git_index_remove_bypath(*this, path.c_str()));
}

void index_wrapper::write()
{
throw_if_error(git_index_write(*this));
Expand Down
3 changes: 3 additions & 0 deletions src/wrapper/index_wrapper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ class index_wrapper : public wrapper_base<git_index>
void write();
git_oid write_tree();

void add_entry(const std::string& path);
void add_entries(std::vector<std::string> patterns);
void add_all();

void remove_entry(const std::string& path);

bool has_conflict() const;
void output_conflicts();
void conflict_cleanup();
Expand Down
1 change: 1 addition & 0 deletions test/conftest_wasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def pytest_ignore_collect(collection_path: pathlib.Path) -> bool:
"test_init.py",
"test_log.py",
"test_merge.py",
"test_mv.py",
"test_rebase.py",
"test_remote.py",
"test_reset.py",
Expand Down
250 changes: 250 additions & 0 deletions test/test_mv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import subprocess

import pytest


def test_mv_basic(xtl_clone, git2cpp_path, tmp_path):
"""Test basic mv operation to rename a file"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create a test file
test_file = xtl_path / "test_file.txt"
test_file.write_text("test content")

# Add the file to git
add_cmd = [git2cpp_path, "add", "test_file.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

# Move/rename the file
mv_cmd = [git2cpp_path, "mv", "test_file.txt", "renamed_file.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode == 0

# Verify the file was moved
assert not test_file.exists()
assert (xtl_path / "renamed_file.txt").exists()

# Check git status
status_cmd = [git2cpp_path, "status", "--long"]
p_status = subprocess.run(status_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_status.returncode == 0
# TODO: uncomment this when the status command is fixed.
#assert "renamed:" in p_status.stdout and "renamed_file.txt" in p_status.stdout


def test_mv_to_subdirectory(xtl_clone, git2cpp_path, tmp_path):
"""Test moving a file to a subdirectory"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create a test file
test_file = xtl_path / "move_me.txt"
test_file.write_text("content to move")

# Add the file to git
add_cmd = [git2cpp_path, "add", "move_me.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

# Move the file to existing subdirectory
mv_cmd = [git2cpp_path, "mv", "move_me.txt", "include/move_me.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode == 0

# Verify the file was moved
assert not test_file.exists()
assert (xtl_path / "include" / "move_me.txt").exists()

# Check git status
status_cmd = [git2cpp_path, "status", "--long"]
p_status = subprocess.run(status_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_status.returncode == 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't check anything in the output here.

# TODO: uncomment this when the status command is fixed.
#assert "renamed:" in p_status.stdout and "move_me.txt" in p_status.stdout


def test_mv_destination_exists_without_force(xtl_clone, git2cpp_path, tmp_path):
"""Test that mv fails when destination exists without --force flag"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create source file
source_file = xtl_path / "source.txt"
source_file.write_text("source content")

# Create destination file
dest_file = xtl_path / "destination.txt"
dest_file.write_text("destination content")

# Add both files to git
add_cmd = [git2cpp_path, "add", "source.txt", "destination.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

# Try to move without force - should fail
mv_cmd = [git2cpp_path, "mv", "source.txt", "destination.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode != 0
assert "destination already exists" in p_mv.stderr

# Verify source file still exists
assert source_file.exists()
assert dest_file.exists()


@pytest.mark.parametrize("force_flag", ["-f", "--force"])
def test_mv_destination_exists_with_force(xtl_clone, git2cpp_path, tmp_path, force_flag):
"""Test that mv succeeds when destination exists with --force flag"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create source file
source_file = xtl_path / "source.txt"
source_file.write_text("source content")

# Create destination file
dest_file = xtl_path / "destination.txt"
dest_file.write_text("destination content")

# Add both files to git
add_cmd = [git2cpp_path, "add", "source.txt", "destination.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

# Move with force - should succeed
mv_cmd = [git2cpp_path, "mv", force_flag, "source.txt", "destination.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode == 0

# Verify source file was moved
assert not source_file.exists()
assert dest_file.exists()
assert dest_file.read_text() == "source content"


def test_mv_nonexistent_source(xtl_clone, git2cpp_path, tmp_path):
"""Test that mv fails when source file doesn't exist"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Try to move a file that doesn't exist
mv_cmd = [git2cpp_path, "mv", "nonexistent.txt", "destination.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode != 0


def test_mv_multiple_files(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
"""Test moving multiple files sequentially"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create test files
file1 = xtl_path / "file1.txt"
file1.write_text("content 1")
file2 = xtl_path / "file2.txt"
file2.write_text("content 2")

# Add files to git
add_cmd = [git2cpp_path, "add", "file1.txt", "file2.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

# Commit the files
commit_cmd = [git2cpp_path, "commit", "-m", "Add test files"]
p_commit = subprocess.run(commit_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_commit.returncode == 0

# Move first file
mv_cmd1 = [git2cpp_path, "mv", "file1.txt", "renamed1.txt"]
p_mv1 = subprocess.run(mv_cmd1, capture_output=True, cwd=xtl_path, text=True)
assert p_mv1.returncode == 0

# Move second file
mv_cmd2 = [git2cpp_path, "mv", "file2.txt", "renamed2.txt"]
p_mv2 = subprocess.run(mv_cmd2, capture_output=True, cwd=xtl_path, text=True)
assert p_mv2.returncode == 0

# Verify both files were moved
assert not file1.exists()
assert not file2.exists()
assert (xtl_path / "renamed1.txt").exists()
assert (xtl_path / "renamed2.txt").exists()


def test_mv_and_commit(xtl_clone, commit_env_config, git2cpp_path, tmp_path):
"""Test moving a file and committing the change"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create a test file
test_file = xtl_path / "original.txt"
test_file.write_text("original content")

# Add and commit the file
add_cmd = [git2cpp_path, "add", "original.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

commit_cmd = [git2cpp_path, "commit", "-m", "Add original file"]
p_commit = subprocess.run(commit_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_commit.returncode == 0

# Move the file
mv_cmd = [git2cpp_path, "mv", "original.txt", "moved.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode == 0

# Check status before commit
status_cmd = [git2cpp_path, "status", "--long"]
p_status = subprocess.run(status_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_status.returncode == 0
assert "Changes to be committed" in p_status.stdout

# Commit the move
commit_cmd2 = [git2cpp_path, "commit", "-m", "Move file"]
p_commit2 = subprocess.run(commit_cmd2, capture_output=True, cwd=xtl_path, text=True)
assert p_commit2.returncode == 0

# Verify the file is in the new location
assert not (xtl_path / "original.txt").exists()
assert (xtl_path / "moved.txt").exists()


def test_mv_nogit(git2cpp_path, tmp_path):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at Ian's last merged PR, there is something to add in this one to test in wasm.

"""Test that mv fails when not in a git repository"""
# Create a test file outside a git repo
test_file = tmp_path / "test.txt"
test_file.write_text("test content")

# Try to mv without being in a git repo
mv_cmd = [git2cpp_path, "mv", "test.txt", "moved.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=tmp_path, text=True)
assert p_mv.returncode != 0


def test_mv_preserve_content(xtl_clone, git2cpp_path, tmp_path):
"""Test that file content is preserved after mv"""
assert (tmp_path / "xtl").exists()
xtl_path = tmp_path / "xtl"

# Create a test file with specific content
test_content = "This is important content that should be preserved"
test_file = xtl_path / "important.txt"
test_file.write_text(test_content)

# Add the file to git
add_cmd = [git2cpp_path, "add", "important.txt"]
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_add.returncode == 0

# Move the file
mv_cmd = [git2cpp_path, "mv", "important.txt", "preserved.txt"]
p_mv = subprocess.run(mv_cmd, capture_output=True, cwd=xtl_path, text=True)
assert p_mv.returncode == 0

# Verify content is preserved
moved_file = xtl_path / "preserved.txt"
assert moved_file.exists()
assert moved_file.read_text() == test_content