use strict; use warnings; use Test::More; use File::Path qw(remove_tree make_path); use File::Spec; use Cwd; use File::Glob qw(:bsd_glob); use constant ROOT => '.vcx'; use constant HEAD => ROOT . '/head'; # Current commit ID use constant OBJ_DIR => ROOT . '/obj'; # Latest version of a file use constant REV_DIR => ROOT . '/rev'; # Commits # Staging area use constant TMP_DIR => ROOT . '/index'; use constant TMP_TREE => TMP_DIR . '/tree'; # Setup Sandbox my $sandbox = "sandbox"; remove_tree($sandbox) if -d $sandbox; make_path($sandbox); my $orig_dir = getcwd(); chdir($sandbox) or die "Cant enter sandbox: $!"; my $cmd = File::Spec->catfile($orig_dir, "vcx"); # Test 'init' ok(system("perl $cmd init > /dev/null") == 0, "Init command ran successfully"); ok(-d ROOT, "ROOT directory created"); ok(-e HEAD, "Head file created"); ok(-d OBJ_DIR, "OBJ_DIR directory created"); ok(-d REV_DIR, "REV_DIR directory created"); # Test 'add' open my $fh, '>', "test.txt" or die $!; print $fh "Hello, world!"; close $fh; ok(system("perl $cmd add test.txt > /dev/null") == 0, "Add command ran successfully"); ok(-d TMP_TREE, "Staging tree created"); # Test 'commit -m' ok(system("perl $cmd commit -m 'Initial commit' > /dev/null") == 0, "Commit with -m ran successfully"); my $rev1 = File::Spec->catdir(REV_DIR, "0000001"); ok(-d $rev1, "Revision 0000001 created"); # Check the message content my $msg_file = File::Spec->catfile($rev1, "message"); ok(-f $msg_file, "Message file exists in revision 1"); open my $msg_fh, '<', $msg_file or die $!; my $saved_msg = <$msg_fh>; chomp $saved_msg; close $msg_fh; is($saved_msg, "Initial commit", "Commit message matches the input"); # Test 'commit -am' (Auto-staging) open my $fh2, '>>', "test.txt" or die $!; print $fh2 "\nMore content"; close $fh2; ok(system("perl $cmd commit -am 'Second commit' > /dev/null") == 0, "Commit -am (auto-staging) ran successfully"); # Check if the object store updated opendir(my $dh, OBJ_DIR) or die $!; my @objs = grep { /[a-f0-9]{40}/ } readdir($dh); closedir($dh); ok(scalar @objs >= 1, "Objects exist in store"); # Shuffle subdirectories make_path("dirA/dirB"); write_file("dirA/dirB/shuffle.txt", "Same Content"); system("perl $cmd add dirA/dirB/shuffle.txt > /dev/null"); system("perl $cmd commit -m 'Shuffle part 1' > /dev/null"); # Move and delete make_path("dirC"); rename("dirA/dirB/shuffle.txt", "dirC/shuffle.txt"); remove_tree("dirA"); # Commit the move system("perl $cmd commit -am 'Shuffle part 2' > /dev/null"); # Verify Revision 4's tree structure my $head = read_file(HEAD); my ($tree_ptr) = bsd_glob(File::Spec->catfile(REV_DIR, $head, "tree-*")); my ($tree_hash) = $tree_ptr =~ /tree-([a-f0-9]{40})$/; my $actual_tree_path = File::Spec->catdir(OBJ_DIR, $tree_hash); # Check for new path ok(-l File::Spec->catfile($actual_tree_path, "dirC/shuffle.txt"), "New path dirC/shuffle.txt exists in object store"); # Check for deleted path (Should NOT exist) ok(!-d File::Spec->catdir($actual_tree_path, "dirA"), "Deleted directory removed from tree snapshot"); # Verify the symlink still points to the correct content blob my $link_target = readlink(File::Spec->catfile($actual_tree_path, "dirC/shuffle.txt")); like($link_target, qr/obj\/[a-f0-9]{40}/, "Symlink in tree points to a valid object blob"); ## Symlink-to-symlink write_file("target.txt", "Final Destination"); symlink("target.txt", "link_a"); symlink("link_a", "link_b"); system("perl $cmd add link_b > /dev/null"); system("perl $cmd commit -m 'Double link' > /dev/null"); $head = read_file(HEAD); ($tree_ptr) = bsd_glob(File::Spec->catfile(REV_DIR, $head, "tree-*")); my $staged_link = File::Spec->catfile(OBJ_DIR, $tree_ptr =~ s/.*tree-//r, "link_b"); is(readlink($staged_link), "link_a", "Symlink-to-symlink preserved literal target"); ## Empty file write_file("empty.txt", ""); system("perl $cmd add empty.txt > /dev/null"); system("perl $cmd commit -m 'Empty file' > /dev/null"); ok(-e File::Spec->catfile(OBJ_DIR, "da39a3ee5e6b4b0d3255bfef95601890afd80709"), "Empty file object created (da39a3...)"); # The overwrite write_file("repeat.txt", "Version A"); system("perl $cmd add repeat.txt > /dev/null"); system("perl $cmd commit -m 'State A' > /dev/null"); my $tree_v1 = (bsd_glob(File::Spec->catfile(REV_DIR, read_file(HEAD), "tree-*")))[0]; write_file("repeat.txt", "Version B"); system("perl $cmd add repeat.txt > /dev/null"); system("perl $cmd commit -m 'State B' > /dev/null"); ## Revert content write_file("repeat.txt", "Version A"); system("perl $cmd add repeat.txt > /dev/null"); system("perl $cmd commit -m 'Back to State A' > /dev/null"); my $tree_v3 = (bsd_glob(File::Spec->catfile(REV_DIR, read_file(HEAD), "tree-*")))[0]; is($tree_v1 =~ s/.*tree-//r, $tree_v3 =~ s/.*tree-//r, "Tree hash reverted perfectly after content restoration"); # Cleanup chdir($orig_dir); # remove_tree($sandbox); # Uncomment this once you're sure it works done_testing(); sub write_file { my ($path, $content) = @_; open my $fh, '>', $path or die "Could not open '$path' for writing: $!"; print $fh $content; close $fh or die "Could not close '$path' after writing: $!"; } sub read_file { my $path = shift; open my $fh, '<', $path or return ""; my $content = <$fh>; chomp $content if $content; close $fh; return $content; }