#!/usr/bin/perl use strict; use warnings; use File::Path qw(make_path); use File::Copy qw(copy); use File::Find; use File::Basename; use File::Glob qw(:bsd_glob); use File::Spec; use Digest::SHA qw(sha1_hex); 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|add]\n"; exit 1; } sub init_repo { make_path(OBJ_DIR, BSE_DIR, TMP_DIR); print "Repository ready\n"; } sub run_status { my $compare_file = sub { return if $File::Find::name =~ /^\.\/\Q${\VCX_DIR}\E/; return if -d $File::Find::name; my $path = File::Spec->abs2rel($File::Find::name, '.'); $path =~ s|^\./||; my $base_path = File::Spec->catfile(BSE_DIR, $path); if (-e $base_path) { if (system("diff -q '$File::Find::name' '$base_path' > /dev/null") != 0) { print "[M] $path" . (check_staged_status($path, 'M') ? " (staged)" : "") . "\n"; } } else { print "[N] $path" . (check_staged_status($path, 'N') ? " (staged)" : "") . "\n"; } }; find({ wanted => $compare_file, no_chdir => 1 }, '.'); find({ wanted => sub { return if -d $_; my $rel = File::Spec->abs2rel($_, BSE_DIR); if (!-e $rel) { print "[D] $rel" . (check_staged_status($rel, 'D') ? " (staged)" : "") . "\n"; } }, no_chdir => 1 }, BSE_DIR); } sub check_staged_status { my ($path, $type) = @_; my $tmp_link = File::Spec->catfile(TMP_DIR, $path); return 0 unless -l $tmp_link; my $staged_target = readlink($tmp_link); 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 (-l $path) { # Compare where the work tree link points vs where the staging link points return (readlink($path) eq $staged_target); } return 0; } sub run_add { my ($target) = @_; my @targets = ($target eq '.') ? ('.') : bsd_glob($target); foreach my $t (@targets) { find({ wanted => sub { return if $File::Find::name =~ /^\.\/\Q${\VCX_DIR}\E/; my $rel = File::Spec->abs2rel($File::Find::name, '.'); $rel =~ s|^\./||; my $tmp_link = File::Spec->catfile(TMP_DIR, $rel); my $base_link = File::Spec->catfile(BSE_DIR, $rel); 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); } elsif (-l $File::Find::name) { _sync_symlink_to_tmp($File::Find::name, $tmp_link); } }, no_chdir => 1, }, $t) if -e $t; _handle_deletions($t); } } # For Regular Files: Copy to OBJ and link to TMP sub _sync_file_to_obj { my ($src, $obj_path_not_used, $tmp) = @_; my $rel_path = File::Spec->abs2rel($src, '.'); $rel_path =~ s|^\./||; my $filename = sha1_hex($rel_path); my $obj = File::Spec->catfile(OBJ_DIR, $filename); make_path(dirname($tmp)); copy($src, $obj) or die "Copy failed: $!"; my $target = File::Spec->abs2rel($obj, dirname($tmp)); unlink($tmp) if -e $tmp || -l $tmp; symlink($target, $tmp) or die "Symlink failed: $!"; print "[Add File] $src (stored as $filename)\n"; } # 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)); 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 = ($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); }