diff options
Diffstat (limited to 'vcx')
| -rw-r--r-- | vcx | 337 |
1 files changed, 234 insertions, 103 deletions
@@ -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; + } +} + |
