#!/usr/bin/perl use strict; use warnings; use File::Path qw(make_path remove_tree); use File::Copy qw(copy); use File::Find; use File::Basename; use File::Glob qw(:bsd_glob); 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 IGNORE_FILE => '.vcxignore'; my $cmd = $ARGV[0] // ''; my $path = $ARGV[1] // ''; if ($cmd eq 'init') { init_repo(); } elsif ($cmd eq 'status') { run_status(); } elsif ($cmd eq 'add') { die "Usage: $0 add [path]\n" unless $path; run_add($path); } else { print "Usage: $0 [init|status]\n"; exit 1; } sub init_repo { make_path(OBJ_DIR, BSE_DIR, TMP_DIR); print "Repository ready\n"; } 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 my $path = File::Spec->abs2rel($File::Find::name, '.'); my $base_path = BSE_DIR . "/$path"; my $tmp_path = TMP_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"; } } else { # New File my $staged = check_staged_status($path, 'N'); print "[N] $path" . ($staged ? " (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"; } }, no_chdir => 1 }, BSE_DIR); } sub check_staged_status { my ($path, $type) = @_; my $tmp = TMP_DIR . "/$path"; 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_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); } } }, follow => 0, no_chdir => 1, }, $t) if -e $t; _handle_deletions($t); } } sub _sync_new_file { my ($src, $obj, $tmp) = @_; make_path(dirname($obj)); 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)); unlink($tmp) if -e $tmp || -l $tmp; symlink($rel_target, $tmp); print "[Add New] $src\n"; } sub _sync_modified_file { my ($src, $obj, $tmp) = @_; my $tmp_obj = "$obj.tmp"; # The modified version in obj/ 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"; } 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); }