summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2026-04-08 23:45:47 +0800
committerSadeep Madurange <sadeep@asciimx.com>2026-04-08 23:45:47 +0800
commit822f4dc2b2ae461f5376467584d9993062c4f428 (patch)
tree8c423cccb2fb3c49c5beed4a23561ac7634addac
parent777ee74b76c17122608b6421b6824f31807ea52b (diff)
downloadcvn-822f4dc2b2ae461f5376467584d9993062c4f428.tar.gz
wip: implement DFS for dirs.
-rw-r--r--vcx370
1 files changed, 66 insertions, 304 deletions
diff --git a/vcx b/vcx
index 1282ef1..0947e4f 100644
--- a/vcx
+++ b/vcx
@@ -15,18 +15,16 @@ use Compress::Zlib;
use POSIX qw(strftime);
use Digest::SHA qw(sha1_hex);
-use constant VCX_DIR => '.vcx';
-use constant HEAD => VCX_DIR . '/head'; # Current commit ID
-use constant OBJ_DIR => VCX_DIR . '/obj'; # Data store: file snapshots, trees
-use constant REV_DIR => VCX_DIR . '/rev'; # Commits
-use constant REG_FILE => VCX_DIR . '/reg'; # File version registry.
+use constant REPO => '.vcx';
+use constant HEAD => REPO . '/head'; # Current revision ID
+use constant INDEX => REPO . '/index'; # Index
+use constant OBJ_DIR => REPO . '/obj'; # Object store
+use constant REV_DIR => REPO . '/rev'; # Revisions
# Staging area
-use constant TMP_DIR => VCX_DIR . '/index';
-use constant TMP_TREE => TMP_DIR . '/tree';
-use constant TMP_META => TMP_DIR . '/meta';
-use constant TMP_DIFF => TMP_DIR . '/delta.tar.gz';
-use constant TMP_REG => REG_FILE . '.tmp';
+use constant TMP_DIR => REPO . '/stg';
+use constant TMP_META => TMP_DIR . '/meta';
+use constant TMP_DIFF => TMP_DIR . '/delta.tar.gz';
Getopt::Long::Configure("bundling");
@@ -54,280 +52,39 @@ if ($cmd eq 'init') {
sub run_init {
make_path(OBJ_DIR, REV_DIR);
- touch_file(REG_FILE);
+ touch_file(INDEX);
- my $initial_hex = to_hex_id(0);
- my $rev0_dir = File::Spec->catfile(REV_DIR, $initial_hex);
- make_path($rev0_dir);
-
- write_file(HEAD, "$initial_hex\n");
+ my $rev_id = to_hex_id(0);
+ my $rev_dir = File::Spec->catfile(REV_DIR, $rev_id);
+ make_path($rev_dir);
+ write_file(HEAD, "$rev_id\n");
# Baseline tree (empty)
- my $empty_tree_hash = sha1_hex("");
- my $empty_tree_file = File::Spec->catfile($rev0_dir, "tree-$empty_tree_hash");
- open my $fh, '>', $empty_tree_file or die $!; close $fh;
- make_path(File::Spec->catdir(OBJ_DIR, $empty_tree_hash));
+ my $tree_hash = sha1_hex("");
+ my $tree_file = File::Spec->catfile($rev_dir, "tree-$tree_hash");
+ open my $fh, '>', $tree_file or die $!; close $fh;
+ make_path(File::Spec->catdir(OBJ_DIR, $tree_hash));
- open my $mfh, '>', File::Spec->catfile($rev0_dir, "message"); close $mfh;
+ open my $mfh, '>', File::Spec->catfile($rev_dir, "message"); close $mfh;
print "Initialized repository.\n";
}
sub run_status {
- open my $fh, '<', HEAD or die "Not a repository.\n";
- my $head = <$fh>; chomp $head; close $fh;
- print "On revision [$head]\n";
-
- my %staged_diffs;
- my $staged_diff_bundle = TMP_DIFF;
-
- if (-e TMP_DIFF) {
- my @list = qx(tar -tf '$staged_diff_bundle');
- foreach (@list) {
- chomp;
- if (/(.+)\.patch$/) { $staged_diffs{$1} = 1; }
+ scan_dir('.', sub {
+ my ($dir, $files) = @_;
+ foreach my $f (@$files) {
+ my $size = $f->{size};
+ my $mtime = $f->{mtime};
+ my $path = File::Spec->catdir($dir, $f->{path});
+ print "$path: $size [$mtime]\n";
}
- }
-
- my ($tree_ptr) = bsd_glob(File::Spec->catfile(REV_DIR, $head, "tree-*"));
- my ($tree_hash) = $tree_ptr =~ /tree-([a-f0-9]{40})$/;
- my $latest_tree = File::Spec->catdir(OBJ_DIR, $tree_hash);
-
- # Pass 1: Workspace -> History (Detects New and Modified)
- find({
- wanted => sub {
- if ($File::Find::name =~ /\/\Q${\VCX_DIR}\E$/ || $_ eq VCX_DIR) {
- $File::Find::prune = 1;
- return;
- }
- return if -d $File::Find::name;
-
- my $rel = File::Spec->abs2rel($File::Find::name, '.');
- $rel =~ s|^\./||;
-
- my $base_in_tree = File::Spec->catfile($latest_tree, $rel);
- my $path_hash = sha1_hex($rel);
-
- if (-e $base_in_tree || -l $base_in_tree) {
- my $obj_in_store = readlink($base_in_tree);
- if (compare($_, $obj_in_store) != 0) {
- my $staged = $staged_diffs{$path_hash} ? " (staged)" : "";
- print "[M] $rel$staged\n";
- }
- } else {
- my $tmp_link = File::Spec->catfile(TMP_TREE, $rel);
- my $staged = (-e $tmp_link || -l $tmp_link) ? " (staged)" : "";
- print "[N] $rel$staged\n";
- }
- },
- no_chdir => 1
- }, '.');
-
- # Pass 2: History -> Workspace (Detects Deletions)
- if (-d $latest_tree) {
- find({
- wanted => sub {
- return if -d $_;
- my $rel = File::Spec->abs2rel($_, $latest_tree);
-
- # If it's in the commit tree but GONE from the workspace
- if (!-e $rel && !-l $rel) {
- my $tmp_link = File::Spec->catfile(TMP_TREE, $rel);
- my $staged = (!-e $tmp_link && !-l $tmp_link) ? " (staged)" : "";
- print "[D] $rel$staged\n";
- }
- },
- no_chdir => 1
- }, $latest_tree);
- }
+ });
}
sub run_add {
- my @targets = @_;
- my %patches;
-
- make_path(TMP_TREE);
-
- open my $fh_h, '<', HEAD or die $!;
- my $head = <$fh_h>; chomp $head; close $fh_h;
-
- my ($latest_tree_ptr) = bsd_glob(File::Spec->catfile(REV_DIR, $head, "tree-*"));
- my ($latest_tree_hash) = $latest_tree_ptr =~ /tree-([a-f0-9]{40})$/;
- my $latest_tree_dir = File::Spec->catdir(OBJ_DIR, $latest_tree_hash);
-
- my %entries;
- open my $afh, '>>', TMP_META or die $!;
-
- init_stage($latest_tree_dir, \%entries);
-
- my $reg = load_registry();
- my $reg_updated = 0;
-
- foreach my $input (@targets) {
- my @expanded = bsd_glob($input);
- foreach my $t (@expanded) {
- find({
- wanted => sub {
- if ($File::Find::name =~ /\/\Q${\VCX_DIR}\E$/ || $_ eq VCX_DIR) {
- $File::Find::prune = 1;
- return;
- }
-
- return if -d $_;
-
- my $rel = $File::Find::name =~ s|^\./||r;
-
- $entries{$rel} = 1;
- my $staged_path = File::Spec->catfile(TMP_TREE, $rel);
- my $prev_link = File::Spec->catfile($latest_tree_dir, $rel);
-
- # CASE 1: Regular File
- if (-f $_ && !-l $_) {
- return if -e $staged_path; # Already staged
-
- if (-l $prev_link) {
- my $obj_in_head = readlink($prev_link);
- return if compare($_, $obj_in_head) == 0;
- }
-
- my $obj_name;
- my $path_hash = sha1_hex($rel);
-
- if (exists $reg->{$path_hash}) {
- $obj_name = $reg->{$path_hash};
- } else {
- $obj_name = hash_file_content($_);
- $reg->{$path_hash} = $obj_name;
- $reg_updated = 1;
- }
-
- my $obj_path = File::Spec->catfile(OBJ_DIR, $obj_name);
-
- # Prepare patches in memory and save to disk in one write.
- make_patch($File::Find::name, $obj_path, \%patches);
- stage_file($File::Find::name, $obj_path, $staged_path);
- print $afh "$rel\0$obj_path\n"; # Record in meta file for the commit command
- }
-
- # CASE 2: Symlink
- elsif (-l _) {
- my $target = readlink($File::Find::name);
- if (-l $prev_link) {
- return if readlink($prev_link) eq $target;
- }
- if (-l $staged_path) {
- return if readlink($staged_path) eq $target;
- }
- stage_link($File::Find::name, $staged_path);
- }
- },
- no_chdir => 1,
- }, $t);
- }
- }
-
- save_patches(\%patches);
-
- if ($reg_updated == 1) {
- write_registry($reg);
- }
-
- # Pass 2: History -> Workspace (Detects Deletions)
- foreach my $path (keys %entries) {
- if (!-e $path && !-l $path) {
- delete $entries{$path};
- my $staged_path = File::Spec->catfile(TMP_TREE, $path);
- if (-e $staged_path || -l $staged_path) {
- unlink($staged_path) or die "Could not unlink staged $path: $!";
- my $parent = dirname($staged_path);
- while ($parent ne TMP_TREE && -d $parent) {
- last if bsd_glob("$parent/*"); # Stop if not empty
- rmdir($parent);
- $parent = dirname($parent);
- }
- }
- print "[D] $path (staged for deletion)\n";
- }
- }
-
- close $afh;
-
- my @sorted_paths = sort keys %entries;
- my $tree_ents = join("\n", @sorted_paths);
- my $tree_header = "tree " . scalar(@sorted_paths) . "\n";
- my $tree_data = $tree_header . $tree_ents;
-
- my $tree_hash = sha1_hex($tree_data);
- my $tree_file = File::Spec->catfile(TMP_DIR, "tree-$tree_hash");
- open my $fh, '>', $tree_file or die $!; close $fh;
}
sub run_commit {
- my ($msg) = @_;
-
- my ($staged_tree_ptr) = bsd_glob(File::Spec->catfile(TMP_DIR, "tree-*"));
- my ($tree_hash) = $staged_tree_ptr =~ /tree-([a-f0-9]{40})$/;
- my $tree_path = File::Spec->catdir(OBJ_DIR, $tree_hash);
- my $tree_exists = -d $tree_path;
-
- my $content_changed = (-e TMP_META || -e TMP_DIFF);
-
- if ($tree_exists && !$content_changed) {
- print "Nothing to commit.";
- File::Path::remove_tree(TMP_DIR) if -d TMP_DIR;
- return;
- }
-
- if (!$msg || $msg eq "") {
- $msg = launch_editor();
- die "Commit aborted: empty message.\n" unless $msg =~ /\S/;
- }
-
- # Prepare IDs
- open my $fh_h, '<', HEAD or die "Not a repository.\n";
- my $old_head = <$fh_h>; chomp $old_head; close $fh_h;
- my $next_id_hex = to_hex_id(from_hex_id($old_head) + 1);
- my $rev_dir = File::Spec->catfile(REV_DIR, $next_id_hex);
- make_path($rev_dir);
-
- # Save file to store
- if (-e TMP_META) {
- open my $mfh, '<', TMP_META or die $!;
- while (my $line = <$mfh>) {
- chomp $line;
- my ($rel, $obj_path) = split("\0", $line);
- copy($rel, $obj_path) or die "Failed to update store for $rel: $!";
- }
- close $mfh;
- }
-
- # Update registry
- if (-e TMP_REG) {
- rename(TMP_REG, REG_FILE) or die "Failed to update registry: $!";
- }
-
- # Save tree (snapshots the structure)
- if (!$tree_exists) {
- make_path($tree_path);
- rename(TMP_TREE, $tree_path) or die "Failed to save directories: $!";
- }
-
- rename($staged_tree_ptr, File::Spec->catfile($rev_dir, "tree-$tree_hash"))
- or die "Failed to save tree pointer to revision: $!";
-
- # Move deltas
- if (-e TMP_DIFF) {
- my $dest_diff = File::Spec->catfile($rev_dir, "delta.tar.gz");
- rename(TMP_DIFF, $dest_diff)
- or die "Failed to move delta to $dest_diff: $!";
- }
-
- write_file(File::Spec->catfile($rev_dir, "message"), "$msg\n");
- write_file(HEAD, "$next_id_hex\n"); # Update head
-
- File::Path::remove_tree(TMP_DIR) if -d TMP_DIR;
-
- my ($subject) = split(/\n/, $msg);
- print "Committed revision [$next_id_hex]: $subject\n";
}
sub run_log {
@@ -500,7 +257,7 @@ sub from_hex_id { hex($_[0]) }
sub launch_editor {
my $editor = $ENV{EDITOR} || $ENV{VISUAL} || 'vi';
- my $temp_msg_file = File::Spec->catfile(VCX_DIR, "COMMIT_EDITMSG");
+ my $temp_msg_file = File::Spec->catfile(REPO, "COMMIT_EDITMSG");
open my $fh, '>', $temp_msg_file or die "Cannot create temp message file: $!";
print $fh "\n# Enter the commit message for your changes.\n";
@@ -539,7 +296,7 @@ sub init_stage {
return if -d _;
my $rel = File::Spec->abs2rel($_, $latest_tree_dir);
- my $staged_path = File::Spec->catfile(TMP_TREE, $rel);
+ my $staged_path = File::Spec->catfile("", $rel);
make_path(dirname($staged_path));
my $target = readlink($_);
@@ -559,39 +316,6 @@ sub touch_file {
close $fh;
}
-sub load_registry {
- my $reg_path = REG_FILE;
- my %reg_data;
-
- return \%reg_data unless -e $reg_path;
-
- open my $fh, '<', $reg_path or die "Could not open registry: $!";
- while (my $line = <$fh>) {
- chomp $line;
- # Split the line: source:target
- # We use a limit of 2 to ensure it only splits at the first dot
- if ($line =~ /^([a-f0-9]{40}):([a-f0-9]{40})$/i) {
- $reg_data{$1} = $2;
- }
- }
- close $fh;
- return \%reg_data;
-}
-
-sub write_registry {
- my ($reg) = @_;
- my $tmp_path = TMP_REG;
-
- open(my $fh, '>', $tmp_path) or die "Could not open $tmp_path for writing: $!";
-
- foreach my $src (sort keys %$reg) {
- my $target = $reg->{$src};
- print $fh "$src:$target\n";
- }
-
- close($fh) or die "Could not close $tmp_path: $!";
-}
-
sub hash_file_content {
my ($filename) = @_;
@@ -602,3 +326,41 @@ sub hash_file_content {
close($fh);
return $sha->hexdigest;
}
+
+sub scan_dir {
+ my ($root, $cb) = @_;
+ my @stack = ($root);
+
+ while (@stack) {
+ my $dir = pop @stack;
+ my $dh;
+
+ unless (opendir($dh, $dir)) {
+ warn "Can't open $dir\n";
+ next;
+ }
+
+ my @files;
+ my @subdirs;
+ while (my $ent = readdir($dh)) {
+ next if $ent eq '.' or $ent eq '..' or $ent eq REPO;
+ my $path = File::Spec->catfile($dir, $ent) =~ s|^\./||r;
+ my @stats = lstat($path);
+ unless (@stats) { warn "Can't lstat $dir\n"; next; }
+ if (-f _ || -l _) {
+ push @files, {
+ path => $path,
+ size => $stats[7],
+ mtime => $stats[9],
+ };
+ } elsif (-d $path) {
+ push @subdirs, $path;
+ }
+ }
+ closedir($dh);
+
+ @files = sort { $a->{path} cmp $b->{path} } @files;
+ $cb->($dir, \@files) if @files;
+ push @stack, sort { $b cmp $a } @subdirs;
+ }
+}