diff options
| author | Sadeep Madurange <sadeep@asciimx.com> | 2026-04-08 23:45:47 +0800 |
|---|---|---|
| committer | Sadeep Madurange <sadeep@asciimx.com> | 2026-04-08 23:45:47 +0800 |
| commit | 822f4dc2b2ae461f5376467584d9993062c4f428 (patch) | |
| tree | 8c423cccb2fb3c49c5beed4a23561ac7634addac /vcx | |
| parent | 777ee74b76c17122608b6421b6824f31807ea52b (diff) | |
| download | cvn-822f4dc2b2ae461f5376467584d9993062c4f428.tar.gz | |
wip: implement DFS for dirs.
Diffstat (limited to 'vcx')
| -rw-r--r-- | vcx | 370 |
1 files changed, 66 insertions, 304 deletions
@@ -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; + } +} |
