summaryrefslogtreecommitdiffstats
path: root/vcx
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2026-04-18 10:47:05 +0800
committerSadeep Madurange <sadeep@asciimx.com>2026-04-18 11:13:29 +0800
commit9908055c17294db977d9a70647ea64dab321292b (patch)
treea2a7467718607e830ebc4eed15e68c440e7ab0c9 /vcx
parenta4258d636fec97688f3db95bd5d1cdf0bd1344f7 (diff)
downloadurn-9908055c17294db977d9a70647ea64dab321292b.tar.gz
Diff command.
Diffstat (limited to 'vcx')
-rw-r--r--vcx337
1 files changed, 234 insertions, 103 deletions
diff --git a/vcx b/vcx
index 951faeb..481a797 100644
--- a/vcx
+++ b/vcx
@@ -24,8 +24,9 @@ use constant OBJ_DIR => REPO . '/obj'; # Object store
use constant REV_DIR => REPO . '/rev'; # Revisions
use constant TMP_DIR => REPO . '/stg'; # Staging area
-use constant MEM_LIMIT => 64 * 1024 * 1024;
-use constant MAX_INDEX_SIZE => 16 * 1024 * 1024;
+use constant CHUNK_LEN => 4096;
+use constant MEM_LIMIT => 10 * 1024 * 1024;
+use constant IO_LAYER => ":raw:perlio(layer=" . CHUNK_LEN . ")";
Getopt::Long::Configure("bundling");
@@ -52,6 +53,8 @@ if ($cmd eq 'init') {
my $file = shift @args;
die "Usage: $0 show <rev_id|HEAD> <file_path>\n" unless defined $rev && defined $file;
run_show($rev, $file);
+} elsif ($cmd eq 'diff') {
+ run_diff(@args);
} else {
print "Usage: $0 [init|status|add|commit|log]\n";
exit 1;
@@ -437,75 +440,119 @@ sub run_log {
sub run_show {
my ($rev_id, $file_path) = @_;
-
- if (!defined $rev_id || lc($rev_id) eq 'head') {
- $rev_id = read_head();
- }
-
- die "Usage: $0 show <rev_id|HEAD> <file_path>\n" unless $rev_id && $file_path;
- my $rev_file = File::Spec->catfile(REV_DIR, $rev_id);
- die "Error: Revision $rev_id not found.\n" unless -f $rev_file;
+ die "Usage: $0 show <rev_id|HEAD> <file_path>\n" unless defined $rev_id && defined $file_path;
- # Extract metadata from revision
- my ($tree_hash, $patch_bundle_hash) = ("", "");
- open my $rfh, '<', $rev_file or die $!;
- while (<$rfh>) {
- if (/^tree:(.*)$/) { $tree_hash = $1; }
- elsif (/^patch:(.*)$/) { $patch_bundle_hash = $1; }
- }
- close $rfh;
+ my $pager = $ENV{PAGER} || 'less -R';
+ open(my $pipe, "| $pager") or die "Can't pipe to $pager: $!";
+ my $old_fh = select($pipe);
- # Locate file in tree
- my $it = stream_tree_file($tree_hash);
- my $target_node;
- while (my $node = $it->()) {
- if ($node->{path} eq $file_path) {
- $target_node = $node;
- last;
+ # 2. Use existing logic to get the content
+ my $v = get_file_version($rev_id, $file_path);
+ die "Error: Could not resolve '$file_path' at revision $rev_id.\n" unless defined $v;
+
+ # 3. Output content
+ if (ref($v) eq 'SCALAR') {
+ binmode STDOUT, ":raw";
+ print $$v;
+ } else {
+ # It's a file path (for large files)
+ open my $fh, '<', $v or die $!;
+ binmode $fh, IO_LAYER;
+ while (read($fh, my $buf, CHUNK_LEN)) {
+ print $buf;
}
+ close $fh;
}
- die "Error: File '$file_path' not found in revision $rev_id.\n" unless $target_node;
+}
- my $obj_path = get_obj_path($target_node->{hash});
- die "Error: Object $target_node->{hash} missing.\n" unless -f $obj_path;
-
- my $content = read_file($obj_path);
+sub run_diff {
+ my @args = @_;
+ my ($src, $dst, $target_path);
- # Apply patch
- if ($patch_bundle_hash) {
- my $bundle_path = get_obj_path($patch_bundle_hash);
- if (-f $bundle_path) {
- open my $bfh, '<:raw', $bundle_path or die $!;
- my $raw_bundle = do { local $/; <$bfh> };
- close $bfh;
+ if (@args == 0) { ($src, $dst) = ('head', undef); }
+ elsif (@args == 1) { is_revision($args[0]) ? (($src, $dst) = ($args[0], undef)) : (($src, $dst, $target_path) = ('head', undef, $args[0])); }
+ elsif (@args == 2) { is_revision($args[0]) && is_revision($args[1]) ? (($src, $dst) = ($args[0], $args[1])) : (($src, $dst, $target_path) = ($args[0], undef, $args[1])); }
+ else { ($src, $dst, $target_path) = @args; }
- # Handle magic bytes (1f 8b = Gzip)
- my $tar_data = (substr($raw_bundle, 0, 2) eq "\x1f\x8b")
- ? Compress::Zlib::uncompress($raw_bundle)
- : $raw_bundle;
+ my ($pipe, $old_fh);
+
+ # Helper to open pager only once when needed
+ my $out = sub {
+ my $msg = shift;
+ if (!defined $pipe && -t STDOUT) {
+ my $pager = $ENV{PAGER} || 'less -R';
+ open($pipe, "| $pager") or die $!;
+ $old_fh = select($pipe);
+ }
+ print $msg;
+ };
- my $tar = Archive::Tar->new;
- $tar->read($tar_data);
-
- my $patch_name = "$file_path.patch";
- if ($tar->contains_file($patch_name)) {
- my $patch_data = $tar->get_content($patch_name);
- if ($patch_data =~ /^\d+(?:,\d+)?[adc]\d+/) {
- my ($tfh, $tpath) = tempfile(DIR => TMP_DIR, UNLINK => 1);
- binmode $tfh, ":raw";
- print $tfh $content;
- close $tfh;
- $content = qx(patch -s -f $tpath -o - <<'EOF'\n$patch_data\nEOF\n);
- } else {
- $content = apply_bin_patch($content, $patch_data);
+ if (defined $target_path) {
+ my $v1 = get_file_version($src, $target_path);
+ my $v2 = get_file_version($dst, $target_path);
+ if (defined $v1 && defined $v2) {
+ my $f1 = ref($v1) ? "<(echo -n " . escapeshellarg($$v1) . ")" : escapeshellarg($v1);
+ my $f2 = ref($v2) ? "<(echo -n " . escapeshellarg($$v2) . ")" : escapeshellarg($v2);
+ if (system("bash", "-c", "diff -q $f1 $f2 > /dev/null 2>&1") != 0) {
+ $out->("\033[1mdiff --vcx a/$target_path b/$target_path\033[0m\n");
+ # Stream the diff output line by line to the pipe
+ open my $dfh, '-|', "bash -c \"diff -u $f1 $f2 | tail -n +3\"";
+ while (<$dfh>) { $out->($_) }
+ close $dfh;
+ }
+ }
+ } else {
+ # Full Tree Walk
+ my $s_id = (lc($src // '') eq 'head') ? read_head() : ($src // '');
+ my $th; if (open my $rf, '<', File::Spec->catfile(REV_DIR, $s_id)) {
+ while (<$rf>) { $th = $1 if /^tree:(.*)$/ } close $rf;
+ }
+ my $it_old = $th ? stream_tree_file($th) : sub { undef };
+ my $it_new = defined $dst ? do {
+ my $d_id = (lc($dst // '') eq 'head') ? read_head() : $dst;
+ my $dth; if (open my $df, '<', File::Spec->catfile(REV_DIR, $d_id)) {
+ while (<$df>) { $dth = $1 if /^tree:(.*)$/ } close $df;
+ }
+ $dth ? stream_tree_file($dth) : sub { undef };
+ } : stream_tree(".");
+
+ my ($old, $new) = ($it_old->(), $it_new->());
+ while ($old || $new) {
+ my $p_old = $old->{path} // '';
+ my $p_new = $new->{path} // '';
+ my $cmp = !defined $old ? 1 : !defined $new ? -1 : $p_old cmp $p_new;
+ if ($cmp == 0) {
+ if (($old->{hash} // '') ne ($new->{hash} // '')) {
+ # Recursively call or just handle diff here
+ my $v1 = get_file_version($src, $p_old);
+ my $v2 = get_file_version($dst, $p_old);
+ if (defined $v1 && defined $v2) {
+ my $f1 = ref($v1) ? "<(echo -n " . escapeshellarg($$v1) . ")" : escapeshellarg($v1);
+ my $f2 = ref($v2) ? "<(echo -n " . escapeshellarg($$v2) . ")" : escapeshellarg($v2);
+ if (system("bash", "-c", "diff -q $f1 $f2 > /dev/null 2>&1") != 0) {
+ $out->("\033[1mdiff --vcx a/$p_old b/$p_old\033[0m\n");
+ open my $dfh, '-|', "bash -c \"diff -u $f1 $f2 | tail -n +3\"";
+ while (<$dfh>) { $out->($_) }
+ close $dfh;
+ }
+ }
}
+ ($old, $new) = ($it_old->(), $it_new->());
+ } elsif ($cmp < 0) {
+ $out->("\033[31m--- $p_old (deleted)\033[0m\n");
+ $old = $it_old->();
+ } else {
+ $out->("\033[32m+++ $p_new (new file)\033[0m\n");
+ $new = $it_new->();
}
}
}
- binmode STDOUT, ":raw";
- print $content;
+ if (defined $pipe) {
+ close $pipe;
+ select($old_fh);
+ }
}
sub make_bin_patch {
@@ -539,6 +586,26 @@ sub make_bin_patch {
return length($patch) > 8 ? $patch : undef;
}
+sub apply_bin_patch {
+ my ($old_data, $patch) = @_;
+ return $old_data unless defined $patch && length($patch) > 8;
+
+ my $new_size = unpack("Q", substr($patch, 0, 8));
+ my $new_data = "\0" x $new_size;
+
+ # Start with existing data as base
+ substr($new_data, 0, length($old_data)) = $old_data;
+
+ my $pos = 8;
+ while ($pos < length($patch)) {
+ my ($offset, $len) = unpack("QL", substr($patch, $pos, 12));
+ $pos += 12;
+ substr($new_data, $offset, $len) = substr($patch, $pos, $len);
+ $pos += $len;
+ }
+ return $new_data;
+}
+
# Convert decimal to a padded 7-char hex string
sub to_hex_id { sprintf("%07x", $_[0]) }
@@ -681,52 +748,6 @@ sub get_obj_path {
return File::Spec->catfile($dir, substr($hash, 2));
}
-sub snapshot_tree {
- my $it = stream_index();
- my @buf;
- my $use_disk = 0;
- my $total_size = 0;
- my $chunk_size = 1024 * 64;
- my $sha = Digest::SHA->new(1);
- my ($tmp_fh, $tmp_path);
-
- while (my $entry = $it->()) {
- my $line = "$entry->{s_hash}\t$entry->{path}\n";
- $sha->add($line);
- $total_size += length($line);
-
- if (!$use_disk && $total_size > MEM_LIMIT) {
- ($tmp_fh, $tmp_path) = tempfile();
- $tmp_fh->setvbuf(undef, POSIX::_IOFBF(), $chunk_size);
- binmode $tmp_fh, ":raw";
- print $tmp_fh @buf;
- @buf = ();
- $use_disk = 1;
- }
-
- if ($use_disk) {
- print $tmp_fh $line;
- } else {
- push @buf, $line;
- }
- }
-
- my $tree_hash = $sha->hexdigest;
- my $obj_path = get_obj_path($tree_hash);
- if (!-e $obj_path) {
- if ($use_disk) {
- close $tmp_fh;
- rename($tmp_path, $obj_path) or die "Rename failed: $!";
- } else {
- write_file($obj_path, join("", @buf));
- }
- } else {
- unlink($tmp_path) if $use_disk;
- }
-
- return $tree_hash;
-}
-
sub stream_tree_file {
my ($hash) = @_;
return sub { return } unless $hash;
@@ -782,3 +803,113 @@ EOF
unlink($tmp_file);
return ($final_msg ne "") ? $final_msg : undef;
}
+
+sub escapeshellarg {
+ my $str = shift;
+ $str =~ s/'/'\\''/g;
+ return "'$str'";
+}
+
+# Check if a string is a 7-character hex revision ID
+sub is_revision {
+ my ($str) = @_;
+ return defined $str && $str =~ /^[0-9a-f]{7}$/i;
+}
+
+sub get_file_version {
+ my ($source, $path) = @_;
+
+ # Default to workspace
+ if (!defined $source) {
+ return undef unless -e $path;
+ if ((-s $path // 0) > MEM_LIMIT) {
+ return $path;
+ }
+ open my $fh, '<', $path or return undef;
+ binmode($fh, IO_LAYER);
+ my $data = do { local $/; <$fh> };
+ close $fh;
+ return \$data;
+ }
+
+ my $rev_id = (lc($source) eq 'head') ? read_head() : $source;
+ my $rev_file = File::Spec->catfile(REV_DIR, $rev_id);
+ return undef unless -f $rev_file;
+
+ my ($tree_hash, $patch_bundle_hash) = ("", "");
+ open my $rfh, '<', $rev_file or return undef;
+ while (<$rfh>) {
+ $tree_hash = $1 if /^tree:(.*)$/;
+ $patch_bundle_hash = $1 if /^patch:(.*)$/;
+ }
+ close $rfh;
+
+ # Locate node in tree
+ my $it = stream_tree_file($tree_hash);
+ my $node;
+ while (my $n = $it->()) {
+ if (($n->{path} // '') eq $path) {
+ $node = $n;
+ last;
+ }
+ }
+ return undef unless $node;
+
+ my $obj_path = get_obj_path($node->{hash} // '');
+ return undef unless -f $obj_path;
+
+ # Extract and patch
+ my ($tfh, $tpath) = tempfile(DIR => TMP_DIR, UNLINK => 1);
+ binmode $tfh, IO_LAYER;
+
+ open my $ofh, '<', $obj_path or return undef;
+ binmode $ofh, IO_LAYER;
+ while (read($ofh, my $buf, CHUNK_LEN)) { print $tfh $buf; }
+ close $ofh;
+ close $tfh;
+
+ if ($patch_bundle_hash) {
+ my $bundle_path = get_obj_path($patch_bundle_hash);
+ if (-f $bundle_path && -s $bundle_path > 0) { # Ensure bundle exists and isn't empty
+ open my $bfh, '<:raw', $bundle_path;
+ my $raw_bundle = do { local $/; <$bfh> };
+ close $bfh;
+
+ my $tar_data = (substr($raw_bundle, 0, 2) eq "\x1f\x8b")
+ ? Compress::Zlib::uncompress($raw_bundle)
+ : $raw_bundle;
+
+ if ($tar_data) {
+ my $tar = Archive::Tar->new;
+ $tar->read($tar_data);
+
+ my $patch_name = "$path.patch";
+ # FIX: Check if the file exists in the tarball before trying to get it
+ if ($tar->contains_file($patch_name)) {
+ my $p_content = $tar->get_content($patch_name);
+
+ my ($pfh, $ppath) = tempfile(DIR => TMP_DIR, UNLINK => 1);
+ binmode $pfh, ":raw";
+ print $pfh $p_content;
+ close $pfh;
+
+ if ($p_content =~ /^\d+(?:,\d+)?[adc]\d+/) {
+ system("patch -s -f $tpath < $ppath >/dev/null 2>&1");
+ } else {
+ my $old_data = read_file($tpath);
+ my $new_data = apply_bin_patch($old_data, $p_content);
+ write_file($tpath, $new_data);
+ }
+ }
+ }
+ }
+ }
+
+ if ((-s $tpath // 0) > MEM_LIMIT) {
+ return $tpath;
+ } else {
+ my $content = read_file($tpath);
+ return \$content;
+ }
+}
+