diff options
| author | Sadeep Madurange <sadeep@asciimx.com> | 2026-03-14 10:11:17 +0800 |
|---|---|---|
| committer | Sadeep Madurange <sadeep@asciimx.com> | 2026-03-14 10:11:17 +0800 |
| commit | 3eccf3989f17668676fb42b73843578b87eef569 (patch) | |
| tree | e3085a79bd6156e676ba976b072f4bab0ab71201 | |
| parent | f0c0e52b896c549007425f42e5fa7d81fa034041 (diff) | |
| download | cvn-3eccf3989f17668676fb42b73843578b87eef569.tar.gz | |
Fix symlink handling in add/status.
| -rw-r--r-- | vcx | 164 |
1 files changed, 67 insertions, 97 deletions
@@ -2,7 +2,7 @@ use strict; use warnings; -use File::Path qw(make_path remove_tree); +use File::Path qw(make_path); use File::Copy qw(copy); use File::Find; use File::Basename; @@ -11,8 +11,8 @@ use File::Spec; use constant VCX_DIR => '.vcx'; use constant BSE_DIR => VCX_DIR . '/bse'; -use constant OBJ_DIR => VCX_DIR . '/obj'; -use constant TMP_DIR => VCX_DIR . '/tmp'; +use constant OBJ_DIR => VCX_DIR . '/obj'; +use constant TMP_DIR => VCX_DIR . '/tmp'; use constant IGNORE_FILE => '.vcxignore'; my $cmd = $ARGV[0] // ''; @@ -26,7 +26,7 @@ if ($cmd eq 'init') { die "Usage: $0 add [path]\n" unless $path; run_add($path); } else { - print "Usage: $0 [init|status]\n"; + print "Usage: $0 [init|status|add]\n"; exit 1; } @@ -36,40 +36,31 @@ sub init_repo { } sub run_status { - # We define a helper to handle the logic for each file encountered my $compare_file = sub { - return if $File::Find::name =~ /^\.\/\Q${\VCX_DIR}\E/; # Skip .vcx - return if -d $File::Find::name; # Directories aren't files + return if $File::Find::name =~ /^\.\/\Q${\VCX_DIR}\E/; + return if -d $File::Find::name; my $path = File::Spec->abs2rel($File::Find::name, '.'); - my $base_path = BSE_DIR . "/$path"; - my $tmp_path = TMP_DIR . "/$path"; + $path =~ s|^\./||; + my $base_path = File::Spec->catfile(BSE_DIR, $path); if (-e $base_path) { - # File exists in both (or was deleted). - # We check diff to see if it's modified. if (system("diff -q '$File::Find::name' '$base_path' > /dev/null") != 0) { - my $staged = check_staged_status($path, 'M'); - print "[M] $path" . ($staged ? " (staged)" : "") . "\n"; + print "[M] $path" . (check_staged_status($path, 'M') ? " (staged)" : "") . "\n"; } } else { - # New File - my $staged = check_staged_status($path, 'N'); - print "[N] $path" . ($staged ? " (staged)" : "") . "\n"; + print "[N] $path" . (check_staged_status($path, 'N') ? " (staged)" : "") . "\n"; } }; - # Walk the working directory find({ wanted => $compare_file, no_chdir => 1 }, '.'); - # Now, find files in BSE_DIR that no longer exist in . (Deletions) find({ wanted => sub { return if -d $_; my $rel = File::Spec->abs2rel($_, BSE_DIR); if (!-e $rel) { - my $staged = check_staged_status($rel, 'D'); - print "[D] $rel" . ($staged ? " (staged)" : "") . "\n"; + print "[D] $rel" . (check_staged_status($rel, 'D') ? " (staged)" : "") . "\n"; } }, no_chdir => 1 @@ -78,117 +69,96 @@ sub run_status { sub check_staged_status { my ($path, $type) = @_; - my $tmp = TMP_DIR . "/$path"; + my $tmp_link = File::Spec->catfile(TMP_DIR, $path); + return 0 unless -l $tmp_link; + + # 1. Get the staging target (what's in .vcx/obj/) + my $staged_target = readlink($tmp_link); + + # 2. CASE: Work tree entry is a SYMLINK + if (-l $path) { + # Compare where the work tree link points vs where the staging link points + return (readlink($path) eq $staged_target); + } + + # 3. CASE: Work tree entry is a REGULAR FILE + if (-f $path) { + # The staged target (e.g., ../obj/path.tmp) must exist to diff + my $abs_target = File::Spec->rel2abs($staged_target, dirname($tmp_link)); + return 0 unless -e $abs_target; + + return (system("diff -q '$path' '$abs_target' > /dev/null") == 0); + } - if ($type eq 'N' || $type eq 'M') { return -e $tmp; } - if ($type eq 'D') { return !-e $tmp; } return 0; } sub run_add { my ($target) = @_; - - # Copy BSE_DIR to TMP_DIR - if (glob(BSE_DIR . "/*")) { - system("cp -R '" . BSE_DIR . "/.' '" . TMP_DIR . "/'"); - } - my @targets = ($target eq '.') ? ('.') : bsd_glob($target); foreach my $t (@targets) { find({ wanted => sub { - # Skip VCX directory return if $File::Find::name =~ /^\.\/\Q${\VCX_DIR}\E/; + my $rel = File::Spec->abs2rel($File::Find::name, '.'); + $rel =~ s|^\./||; - my $rel_path = File::Spec->abs2rel($File::Find::name, '.'); - my $local_path = $File::Find::name; - my $base_path = BSE_DIR . "/$rel_path"; - my $obj_path = OBJ_DIR . "/$rel_path"; - my $tmp_link = TMP_DIR . "/$rel_path"; - - # Regular file - if (-f $local_path) { - if (!-e $base_path) { - _sync_new_file($local_path, $obj_path, $tmp_link); - } elsif (!-l $base_path && system("diff -q '$local_path' '$base_path' > /dev/null") != 0) { - _sync_modified_file($local_path, $obj_path, $tmp_link); - } - } - # Symlink - elsif (-l $local_path) { - if (-l $base_path) { - # Both are symlinks: compare the targets (where they point) - if (readlink($local_path) ne readlink($base_path)) { - _sync_modified_file($local_path, $obj_path, $tmp_link); - } - } else { - # Local is a symlink, but base isn't: treat as modification - _sync_modified_file($local_path, $obj_path, $tmp_link); - } + my $tmp_link = File::Spec->catfile(TMP_DIR, $rel); + my $base_link = File::Spec->catfile(BSE_DIR, $rel); + + # CASE 1: File (Regular) + if (-f $File::Find::name && !-l $File::Find::name) { + my $obj_path = File::Spec->catfile(OBJ_DIR, $rel . ".tmp"); + _sync_file_to_obj($File::Find::name, $obj_path, $tmp_link); } + # CASE 2: Symlink + elsif (-l $File::Find::name) { + _sync_symlink_to_tmp($File::Find::name, $tmp_link); + } }, - follow => 0, no_chdir => 1, }, $t) if -e $t; - + _handle_deletions($t); } } -sub _sync_new_file { +# For Regular Files: Copy to OBJ and link to TMP +sub _sync_file_to_obj { my ($src, $obj, $tmp) = @_; + + # 1. Ensure the directory path to the OBJ file exists make_path(dirname($obj)); + # 2. Ensure the directory path to the TMP symlink exists make_path(dirname($tmp)); + copy($src, $obj); - my $abs_obj = File::Spec->rel2abs($obj); - my $abs_tmp = File::Spec->rel2abs($tmp); - my $rel_target = File::Spec->abs2rel($abs_obj, dirname($abs_tmp)); + # Create relative link from TMP to OBJ + my $rel_target = File::Spec->abs2rel(File::Spec->rel2abs($obj), dirname($tmp)); unlink($tmp) if -e $tmp || -l $tmp; symlink($rel_target, $tmp); - print "[Add New] $src\n"; + print "[Add File] $src\n"; } -sub _sync_modified_file { - my ($src, $obj, $tmp) = @_; - my $tmp_obj = "$obj.tmp"; # The modified version in obj/ +# For Symlinks: Mirror the symlink into TMP +sub _sync_symlink_to_tmp { + my ($src, $tmp) = @_; + my $target = readlink($src); # Read where the original points - make_path(dirname($tmp_obj)); - copy($src, $tmp_obj); - - unlink($tmp) if -e $tmp || -l $tmp; # Ensure clean slate - - # RELATIVE PATH FIX: - # Link: .vcx/tmp/path/to/file (at least 2+ levels deep) - # Target: .vcx/obj/path/to/file.tmp - # We need to get out of tmp/ and into obj/ - my $rel_target = "../obj/" . $src . ".tmp"; - - symlink($rel_target, $tmp); - print "[Add Mod] $src\n"; + make_path(dirname($tmp)); + unlink($tmp) if -e $tmp || -l $tmp; + symlink($target, $tmp); # Create exact same link in TMP + print "[Add Link] $src -> $target\n"; } sub _handle_deletions { my ($target) = @_; - my $search_base = ($target eq '.') ? BSE_DIR : BSE_DIR . "/$target"; - - return unless -d $search_base || -e $search_base; - - find({ - wanted => sub { - return if -d $_; - my $rel_path = File::Spec->abs2rel($File::Find::name, BSE_DIR); - - # If file exists in BSE_DIR but NOT in current directory (.) - if (!-e $rel_path) { - unlink(OBJ_DIR . "/$rel_path"); - unlink(BSE_DIR . "/$rel_path"); - print "[Deleted] $rel_path\n"; - } - }, - no_chdir => 1, - }, $search_base); + my $search = ($target eq '.') ? BSE_DIR : File::Spec->catfile(BSE_DIR, $target); + return unless -d $search || -e $search; + find({ wanted => sub { return if -d $_; my $rel = File::Spec->abs2rel($_, BSE_DIR); + if (!-e $rel) { unlink(File::Spec->catfile(OBJ_DIR, $rel), $_); print "[Deleted] $rel\n"; } + }, no_chdir => 1 }, $search); } - |
