summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2026-03-14 10:11:17 +0800
committerSadeep Madurange <sadeep@asciimx.com>2026-03-14 10:11:17 +0800
commit3eccf3989f17668676fb42b73843578b87eef569 (patch)
treee3085a79bd6156e676ba976b072f4bab0ab71201
parentf0c0e52b896c549007425f42e5fa7d81fa034041 (diff)
downloadcvn-3eccf3989f17668676fb42b73843578b87eef569.tar.gz
Fix symlink handling in add/status.
-rw-r--r--vcx164
1 files changed, 67 insertions, 97 deletions
diff --git a/vcx b/vcx
index 184cec5..2c95691 100644
--- a/vcx
+++ b/vcx
@@ -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);
}
-