diff options
| -rw-r--r-- | vcx | 216 |
1 files changed, 160 insertions, 56 deletions
@@ -58,26 +58,7 @@ if ($cmd eq 'init') { sub run_init { make_path(OBJ_DIR, REV_DIR, TMP_DIR); touch_file(INDEX); - my $rev_id = to_hex_id(0); - my $rev_file = File::Spec->catfile(REV_DIR, $rev_id); - - my $empty_tree = sha1_hex(""); - touch_file(File::Spec->catfile(OBJ_DIR, $empty_tree)); - - my $ts = time(); - my $msg = "Revision zero.\n"; - my $msg_len = length($msg); - - open my $fh, '>:raw', $rev_file or die $!; - print $fh "tree:$empty_tree\n"; - print $fh "parent:none\n"; - print $fh "date:$ts\n"; - print $fh "len:$msg_len\n"; - print $fh "\n"; # The "Header/body" separator - print $fh $msg; - close $fh; - write_file(HEAD, "$rev_id\n"); print "Initialized repository.\n"; } @@ -163,6 +144,118 @@ sub run_add { } sub run_commit { + my ($msg) = @_; + + # Get message and parent + $msg //= launch_editor(); + die "Commit aborted: empty message\n" unless $msg && $msg =~ /\S/; + + my $parent_id = read_head(); + + my $it = stream_index(); + my %patches; + + # New index (temporary) + my ($idx_fh, $idx_path) = tempfile(DIR => TMP_DIR, UNLINK => 0); + binmode $idx_fh, ":raw"; + + # Tree logic + my $sha_tree = Digest::SHA->new(1); + my @tree_lines; + my $tree_size = 0; + my $use_disk_tree = 0; + my ($tree_fh, $tree_path); + + while (my $entry = $it->()) { + my $final_hash_for_tree = $entry->{base_hash}; + + if ($entry->{staged_hash} ne $entry->{base_hash}) { + my $patch = calculate_delta($entry); + my $compressed = compress_data($patch); + + # THE PIVOT DECISION (50% Rule) + if (length($compressed) < ($entry->{size} * 0.5)) { + # Efficiency Win: Store Patch + $patches{"$entry->{path}.patch"} = $compressed; + # Tree continues to point to OLD base (maintaining delta chain) + $final_hash_for_tree = $entry->{base_hash}; + } else { + # Pivot: Store full object as a new Keyframe + my $obj_path = File::Spec->catfile(OBJ_DIR, $entry->{staged_hash}); + if (!-e $obj_path) { + copy($entry->{path}, $obj_path) or die "Pivot failed: $!"; + } + # Tree points to NEW hash (resetting delta chain) + $final_hash_for_tree = $entry->{staged_hash}; + } + } + + # --- A. Update Tree State --- + my $t_line = "$final_hash_for_tree\t$entry->{path}\n"; + $sha_tree->add($t_line); + $tree_size += length($t_line); + + if (!$use_disk_tree && $tree_size > MEM_LIMIT) { + ($tree_fh, $tree_path) = tempfile(DIR => TMP_DIR, UNLINK => 0); + binmode $tree_fh, ":raw"; + print $tree_fh @tree_lines; + @tree_lines = (); + $use_disk_tree = 1; + } + $use_disk_tree ? print $tree_fh $t_line : push @tree_lines, $t_line; + + # --- B. Update Index State --- + # Current staged_hash becomes the next base_hash + # We also refresh metadata from the actual file + my @stats = stat($entry->{path}); + print $idx_fh join("\t", + $entry->{staged_hash}, # New Staged + $entry->{staged_hash}, # New Base (Index always tracks "Current") + $stats[9], # Fresh mtime + $stats[7], # Fresh size + $entry->{path} + ) . "\n"; + } + + # 4. Finalize Tree Object + my $tree_hash = $sha_tree->hexdigest; + my $final_tree_obj = File::Spec->catfile(OBJ_DIR, $tree_hash); + + if (!-e $final_tree_obj) { + if ($use_disk_tree) { + close $tree_fh; + rename($tree_path, $final_tree_obj) or die $!; + } else { + write_file($final_tree_obj, join("", @tree_lines)); + } + } else { + unlink($tree_path) if $use_disk_tree; + } + + # 5. Assemble Unified Revision File + my $next_id = to_hex_id(from_hex_id($parent_id) + 1); + my $rev_file = File::Spec->catfile(REV_DIR, $next_id); + + my $tar_payload = create_tarball(\%patches); + my $ts = time(); + my $msg_final = "$msg\n"; + my $msg_len = length($msg_final); + + open my $rfh, '>:raw', $rev_file or die $!; + print $rfh "tree:$tree_hash\n"; + print $rfh "parent:$parent_id\n"; + print $rfh "date:$ts\n"; + print $rfh "len:$msg_len\n\n"; + print $rfh $msg_final; + print $rfh $tar_payload; + close $rfh; + + # 6. Final Atomicity: Commit Index and HEAD + close $idx_fh; + rename($idx_path, INDEX) or die "Index update failed: $!"; + write_file(HEAD, "$next_id\n"); + + print "Revision $next_id committed.\n"; } sub run_log { @@ -209,30 +302,6 @@ sub run_log { select($old_fh); } -sub stage_file { - my ($src, $obj, $tmp) = @_; - make_path(dirname($tmp)); - - # We want the link inside tmp/ to point to "obj/HASH" - # relative to project root. - my $rel_target = $obj; - - unlink($tmp) if -e $tmp || -l $tmp; - symlink($rel_target, $tmp) or die "Failed to symlink $tmp: $!"; - print "[Staged File] $src\n"; -} - -sub stage_link { - my ($src, $tmp) = @_; - my $target = readlink($src); - - make_path(dirname($tmp)); - unlink($tmp) if -e $tmp || -l $tmp; - # For workspace symlinks, we clone the target - symlink($target, $tmp) or die "Failed to symlink $tmp: $!"; - print "[Staged link] $src -> $target\n"; -} - sub make_patch { my ($src, $obj_path, $patches) = @_; @@ -314,19 +383,6 @@ sub apply_bin_patch { close $fh; } -sub save_patches { - my ($patches) = @_; - - return unless keys %$patches; - - my $tar = Archive::Tar->new; - while (my ($obj_name, $patch) = each %$patches) { - $tar->add_data("$obj_name.patch", $patch); - } - - $tar->write(TMP_DIFF, 1); # gzip -} - # Convert decimal to a padded 7-char hex string sub to_hex_id { sprintf("%07x", $_[0]) } @@ -500,3 +556,51 @@ sub matches_paths { return 0; } +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 = File::Spec->catfile(OBJ_DIR, $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("", @buffer)); + } + } else { + unlink($tmp_path) if $use_disk; + } + + return $tree_hash; +} + |
