#!/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'; 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 { my $diff_cmd = "diff -x " . VCX_DIR . " -rq " . BSE_DIR . " ."; $diff_cmd .= " -X .vcxignore" if -e ".vcxignore"; my @output = `$diff_cmd`; foreach my $line (@output) { chomp $line; # Format output if ($line =~ /^Only in \Q@{[BSE_DIR]}\E: (.+)$/) { print "[D] $1\n"; } elsif ($line =~ /^Only in \.: (.+)$/) { print "[N] $1\n"; } elsif ($line =~ /^Files \Q@{[BSE_DIR]}\E\/(.+) and \.\/(.+) differ$/) { print "[M] $1\n"; } } } sub run_add { my ($target) = @_; # Copy BSE_DIR to TMP_DIR remove_tree(TMP_DIR); make_path(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); # Symlink from tmp/path/to/file to obj/path/to/file # Note: symlink target is relative to the symlink's location symlink("../../obj/" . $src, $tmp); print "[Add New] $src\n"; } sub _sync_modified_file { my ($src, $obj, $tmp) = @_; my $tmp_obj = "$obj.tmp"; make_path(dirname($tmp_obj)); copy($src, $tmp_obj); unlink($tmp) if -l $tmp; symlink("../../obj/" . $src . ".tmp", $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); }