summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2026-04-04 11:55:08 +0800
committerSadeep Madurange <sadeep@asciimx.com>2026-04-04 14:52:02 +0800
commitcf421aeff4faa15a2a80495345ae76023830ca86 (patch)
treed417dd85f75fc4c532bb0954ed59bea87d350914
parent3af329abee56900dd42cb305bd7a431ab3ded90a (diff)
downloadcvn-cf421aeff4faa15a2a80495345ae76023830ca86.tar.gz
Unit tests and fix hash collisions, deletions.
-rw-r--r--.gitignore1
-rw-r--r--Makefile21
-rw-r--r--main.c508
-rw-r--r--test_vcx.t167
-rw-r--r--vcx42
5 files changed, 200 insertions, 539 deletions
diff --git a/.gitignore b/.gitignore
index d1c9982..6257f3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
cvn
.cvn/
test_env*/
+sandbox*/
**/*.o
**/*.out
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9104cae..0000000
--- a/Makefile
+++ /dev/null
@@ -1,21 +0,0 @@
-CC = cc
-TARGET = cvn
-
-SRC = main.c
-OBJ = $(SRC:.c=.o)
-
-CFLAGS = -std=c99 -O3 -Wall -I/usr/local/include
-LDFLAGS = -L/usr/local/lib
-
-all: $(TARGET)
-
-%.o: %.c
- $(CC) $(CFLAGS) -c $< -o $@
-
-$(TARGET): $(OBJ)
- $(CC) $(OBJ) -o $(TARGET) $(LDFLAGS)
-
-.PHONY: clean
-
-clean:
- rm -f $(OBJ) $(TARGET)
diff --git a/main.c b/main.c
deleted file mode 100644
index 5202715..0000000
--- a/main.c
+++ /dev/null
@@ -1,508 +0,0 @@
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <fnmatch.h>
-#include <ftw.h>
-#include <limits.h>
-#include <sha1.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/wait.h>
-
-#define VCX_DIR ".vcx"
-#define HEAD VCX_DIR "/head"
-#define TMP_DIR VCX_DIR "/tmp"
-#define OBJ_DIR VCX_DIR "/obj"
-
-#define EXCLUDE_PATHS ".vcxignore"
-
-#define MAX_DEPTH 256
-#define BUF_LEN 8192
-#define PATH_LEN PATH_MAX
-#define HASH_LEN SHA1_DIGEST_STRING_LENGTH
-
-#define KIND_DEL 'D'
-#define KIND_MOD 'M'
-#define KIND_NEW 'N'
-
-#define MALLOC(s) _xmalloc((s), __FILE__, __LINE__)
-#define REALLOC(p, s) _xrealloc((p), (s), __FILE__, __LINE__)
-
-static inline void init(int argc, char *argv[]);
-static inline void status(int argc, char *argv[]);
-static inline void add(int argc, char *argv[]);
-
-static inline int cmplink(const char *link1, const char *link2);
-static inline int cmpfile(const char *file1, const char *file2);
-
-static inline void stage_init(void);
-static inline void stage_entry(const char *path, char kind, int islnk);
-static inline void print_status(const char *file, char kind, int islnk);
-
-static inline int scan_head(const char * path, const struct stat *st, int flag, struct FTW *ftwbuf);
-static inline int scan_tree(const char * path, const struct stat *st, int flag, struct FTW *ftwbuf);
-
-static inline void mkcopy(const char *path, char *dst);
-static inline void mklink(const char *src, const char *obj);
-
-static inline void mkdirs(const char *path);
-static inline void copy_link(const char *src);
-static inline void copy_file(const char *src, const char *dst);
-
-static inline int lnklen(const char *lnk);
-static inline char *format_path(const char *fmt, ...);
-
-static inline void *_xmalloc(size_t s, const char *file, int line);
-static inline void *_xrealloc(void *ptr, size_t s, const char *file, int line);
-
-struct command {
- char *name;
- void (*func)(int argc, char *argv[]);
-};
-
-struct command cmd[] = {
- {"init", init},
- {"add", add},
- {"status", status},
- {NULL, NULL}
-};
-
-int main(int argc, char *argv[])
-{
- if (argc < 2)
- errx(1, "Usage: %s <command> [<args>]", argv[0]);
-
- for (int i = 0; cmd[i].name != NULL; i++) {
- if (strcmp(argv[1], cmd[i].name) == 0) {
- cmd[i].func(argc - 1, argv + 1);
- return 0;
- }
- }
- return 0;
-}
-
-static inline void init(int argc, char *argv[])
-{
- if (mkdir(VCX_DIR, 0755) == -1) {
- if (errno != EEXIST)
- err(1, "Failed to create %s", VCX_DIR);
- }
-
- if (mkdir(HEAD, 0755) == -1) {
- if (errno != EEXIST)
- err(1, "Failed to create %s", HEAD);
- }
-
- if (mkdir(OBJ_DIR, 0755) == -1) {
- if (errno != EEXIST)
- err(1, "Failed to create %s", OBJ_DIR);
- }
-
- printf("Repository ready\n");
-}
-
-static void (*scan_res_cb)(const char *file, char kind, int islnk) = NULL;
-
-static inline void status(int argc, char *argv[])
-{
- scan_res_cb = print_status;
-
- if (nftw(HEAD, scan_head, MAX_DEPTH, FTW_PHYS) == -1)
- err(1, "Failed to scan head");
-
- if (nftw(".", scan_tree, MAX_DEPTH, FTW_PHYS) == -1)
- err(1, "Failed to scan work tree");
-}
-
-static inline void add(int argc, char *argv[])
-{
- size_t i;
- struct stat st;
- char *wt_path, *hd_path;
-
- if (argc < 2)
- errx(1, "Usage: %s [<files>]", argv[0]);
-
- stage_init();
- scan_res_cb = stage_entry;
-
- for (i = 1; i < argc; i++) {
- wt_path = argv[i];
- if (lstat(wt_path, &st) == -1)
- err(1, "%s", wt_path);
-
- hd_path = format_path("%s/%s", HEAD, wt_path);
-
- if (nftw(hd_path, scan_head, MAX_DEPTH, FTW_PHYS) == -1)
- err(1, "Failed to scan head");
-
- if (nftw(wt_path, scan_tree, MAX_DEPTH, FTW_PHYS) == -1)
- err(1, "Failed to scan work tree");
- }
-}
-
-int scan_head(const char * path, const struct stat *sb, int flag,
- struct FTW *ftwbuf)
-{
- char *wt_path;
- struct stat wt_stat;
-
- if (flag == FTW_F) {
- const char *filename = path + ftwbuf->base;
- wt_path = format_path("./%s", filename);
-
- if (lstat(wt_path, &wt_stat) == -1) {
- if (errno == ENOENT) {
- // file/link doesn't matter for deletions
- scan_res_cb(wt_path, KIND_DEL, 0);
- return 0;
- }
- else
- err(1, "lstat failed for %s", wt_path);
- }
-
- if (wt_stat.st_size == sb->st_size &&
- wt_stat.st_mtime == sb->st_mtime)
- return 0; // no change
-
- if (S_ISREG(wt_stat.st_mode)) {
- if (cmpfile(path, wt_path) == 1)
- scan_res_cb(wt_path, KIND_MOD, 0);
- } else if (S_ISLNK(wt_stat.st_mode)) {
- if (cmplink(path, wt_path) == 1)
- scan_res_cb(wt_path, KIND_MOD, 1);
- }
- }
-
- return 0;
-}
-
-static inline int scan_tree(const char * path, const struct stat *st,
- int flag, struct FTW *ftwbuf)
-{
- char *hd_path;
-
- if (fnmatch("./.vcx/*", path, 0) == 0)
- return 0;
-
- if (flag == FTW_F) {
- const char *filename = path + ftwbuf->base;
- hd_path = format_path("%s/%s", HEAD, filename);
- if (access(hd_path, F_OK) != 0)
- scan_res_cb(path, KIND_NEW, S_ISLNK(st->st_mode) != 0);
- }
-
- return 0;
-}
-
-static inline int cmpfile(const char *file1, const char *file2)
-{
- int rc;
- int fd1, fd2;
- ssize_t r1, r2;
- char buf1[BUF_LEN], buf2[BUF_LEN];
-
- if ((fd1 = open(file1, O_RDONLY)) < 0)
- err(1, "Couldn't open %s to compare", file1);
-
- if ((fd2 = open(file2, O_RDONLY)) < 0)
- err(1, "Couldn't open %s to compare", file2);
-
- rc = 0;
- do {
- r1 = read(fd1, buf1, BUF_LEN);
- r2 = read(fd2, buf2, BUF_LEN);
-
- if (r1 != r2 || memcmp(buf1, buf2, r1) != 0) {
- rc = 1;
- break;
- }
- } while (r1 > 0);
-
- close(fd1);
- close(fd2);
- return rc;
-}
-
-static inline int cmplink(const char *link1, const char *link2)
-{
- ssize_t len1, len2;
- int buflen1, buflen2;
-
- buflen1 = lnklen(link1);
- char buf1[buflen1];
- len1 = readlink(link1, buf1, sizeof(buf1) - 1);
-
- buflen2 = lnklen(link2);
- char buf2[buflen2];
- len2 = readlink(link2, buf2, sizeof(buf2) - 1);
-
- if (len1 < 0 || len2 < 0)
- err(1, "readlink error on %s or %s", link1, link2);
-
- if (len1 != len2)
- return 1;
-
- buf1[len1] = '\0';
- buf2[len2] = '\0';
-
- return strcmp(buf1, buf2);
-}
-
-static inline void stage_init(void)
-{
- pid_t rm_pid, cp_pid;
-
- rm_pid = fork();
- if (rm_pid == 0) {
- if (access(TMP_DIR, F_OK) == 0) {
- execlp("rm", "rm", "-rf", TMP_DIR, NULL);
- err(1, "Failed to delete %s", TMP_DIR);
- }
- _exit(0);
- } else {
- waitpid(rm_pid, NULL, 0);
- cp_pid = fork();
- if (cp_pid == 0) {
- execlp("cp", "cp", "-RP", HEAD, TMP_DIR, NULL);
- err(1, "Failed to copy %s to %s", HEAD, TMP_DIR);
- }
- waitpid(cp_pid, NULL, 0);
- }
-}
-
-static inline void stage_entry(const char *path, char kind, int islnk)
-{
- char objname[HASH_LEN];
-
- switch (kind) {
- case KIND_MOD:
- break;
- case KIND_NEW:
- if (!islnk) {
- mkcopy(path, objname);
- mklink(path, objname);
- } else
- copy_link(path);
- break;
- case KIND_DEL:
- break;
- default:
- break;
- }
-}
-
-static inline void mkcopy(const char *path, char *dst)
-{
- int len;
- char *obj_path;
-
- len = strlen(path);
-
- if (SHA1Data((const uint8_t *)path, len, dst) == NULL)
- err(1, "Failed to compute hash for %s", path);
-
- obj_path = format_path("%s/%s.tmp", OBJ_DIR, dst);
- copy_file(path, obj_path);
-}
-
-static inline void mklink(const char *src, const char *obj)
-{
- char *lnk, *dst;
-
- lnk = format_path("../obj/%s.tmp", obj);
- dst = format_path("%s/%s", TMP_DIR, src);
-
- mkdirs(dst);
-
- if (symlink(lnk, dst) != 0)
- err(1, "symlink %s", src);
-}
-
-static inline void print_status(const char *file, char kind, int islnk)
-{
- printf("[%c] %s\n", kind, file);
-}
-
-static inline int lnklen(const char *lnk)
-{
- int len;
- struct stat st;
-
- if (lstat(lnk, &st) != 0)
- err(1, "lstat %s", lnk);
-
- len = st.st_size + 1;
- if (PATH_LEN < len)
- errx(1, "Link too long: %s", lnk);
-
- return len;
-}
-
-static inline int diff(const char *file1, const char *file2)
-{
- pid_t pid;
- int null_fd, status, rc;
-
- pid = fork();
-
- if (pid == 0) {
- null_fd = open("/dev/null", O_WRONLY);
- if (null_fd == -1)
- err(1, "open() failed on /dev/null");
-
- dup2(null_fd, STDOUT_FILENO);
- dup2(null_fd, STDERR_FILENO);
- close(null_fd);
-
- execlp("diff", "diff", "-q", file1, file2, (char *)NULL);
- err(1, "execlp failed for diff");
- } else if (pid > 0) {
- waitpid(pid, &status, 0);
- // diff returns 0 if files are same, 1 if different, >1 if error
- rc = WEXITSTATUS(status);
- if (rc > 1)
- err(1, "diff error");
- return rc;
- }
-
- err(1, "fork");
-}
-
-static inline void copy_file(const char *src, const char *dst)
-{
- int fdin, fdout;
- struct stat st;
- char buf[BUF_LEN], *outptr;
- ssize_t nread, nwrite, res;
-
- if ((fdin = open(src, O_RDONLY)) < 0)
- err(1, "open failed for %s", src);
-
- if (fstat(fdin, &st) < 0) {
- close(fdin);
- err(1, "fstat failed for %s", src);
- }
-
- mkdirs(dst);
-
- fdout = open(dst, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode & 0777);
- if (fdout < 0) {
- close(fdin);
- err(1, "open failed for %s", dst);
- }
-
- while ((nread = read(fdin, buf, sizeof(buf))) > 0) {
- nwrite = 0;
- outptr = buf;
-
- // Handle partial writes (possible with large files/slow disks)
- while (nwrite < nread) {
- res = write(fdout, outptr + nwrite, nread - nwrite);
- if (res < 0) {
- if (errno == EINTR) // interrupted by a signal - retry
- continue;
- close(fdin);
- close(fdout);
- err(1, "Copy error on %s", src); // bad error: e.g, disk full
- }
- nwrite += res;
- }
- }
-
- close(fdin);
- close(fdout);
-}
-
-static inline void copy_link(const char *src)
-{
- int len;
- ssize_t n;
- char *dst;
-
- // todo: use path_buf
- len = lnklen(src);
- char target[len];
- n = readlink(src, target, len);
- if (n == -1)
- err(1, "readlink %s", src);
-
- target[n] = '\0';
-
- dst = format_path("%s/%s", TMP_DIR, src);
- mkdirs(dst);
-
- if (symlink(target, dst) != 0)
- err(1, "Link copy error %s", src);
-}
-
-static inline void mkdirs(const char *path)
-{
- char *p;
- int pathlen;
-
- static char buf[PATH_LEN];
-
- pathlen = strlen(path);
- if (PATH_LEN < pathlen + 1)
- errx(1, "Path too long: %s", path);
-
- buf[0] = '\0';
- strcpy(buf, path);
-
- for (p = buf; *p; p++) {
- if (*p == '/') {
- *p = '\0';
- if (mkdir(buf, 0755) != 0) {
- if (errno != EEXIST)
- err(1, "mkdir failed for %s", buf);
- }
- *p = '/';
- }
- }
-}
-
-static char *path_buf[2][PATH_LEN];
-
-static inline char *format_path(const char *fmt, ...)
-{
- int rc;
- va_list args;
- static uint8_t i = 1;
-
- i ^= 1;
- va_start(args, fmt);
- rc = vsnprintf((char *)path_buf[i], PATH_LEN, fmt, args);
- va_end(args);
-
- if (rc < 0)
- err(1, "vsnprintf");
-
- if (rc >= PATH_LEN)
- errx(1, "Path too long");
-
- return (char *)path_buf[i];
-}
-
-static inline void *_xmalloc(size_t s, const char *file, int line)
-{
- void *p;
-
- if (!(p = malloc(s)))
- err(1, "%s:%d: malloc", file, line);
- return p;
-}
-
-static inline void *_xrealloc(void *ptr, size_t s, const char *file,
- int line)
-{
- void *p;
-
- if (!(p = realloc(ptr, s)))
- err(1, "%s:%d: realloc", file, line);
- return p;
-}
-
diff --git a/test_vcx.t b/test_vcx.t
new file mode 100644
index 0000000..e619824
--- /dev/null
+++ b/test_vcx.t
@@ -0,0 +1,167 @@
+use strict;
+use warnings;
+use Test::More;
+use File::Path qw(remove_tree make_path);
+use File::Spec;
+use Cwd;
+use File::Glob qw(:bsd_glob);
+
+use constant ROOT => '.vcx';
+use constant HEAD => ROOT . '/head'; # Current commit ID
+use constant OBJ_DIR => ROOT . '/obj'; # Latest version of a file
+use constant REV_DIR => ROOT . '/rev'; # Commits
+
+# Staging area
+use constant TMP_DIR => ROOT . '/index';
+use constant TMP_TREE => TMP_DIR . '/tree';
+
+# Setup Sandbox
+my $sandbox = "sandbox";
+remove_tree($sandbox) if -d $sandbox;
+make_path($sandbox);
+
+my $orig_dir = getcwd();
+chdir($sandbox) or die "Cant enter sandbox: $!";
+
+my $cmd = File::Spec->catfile($orig_dir, "vcx");
+
+# Test 'init'
+ok(system("perl $cmd init") == 0, "Init command ran successfully");
+ok(-d ROOT, "ROOT directory created");
+ok(-e HEAD, "Head file created");
+ok(-d OBJ_DIR, "OBJ_DIR directory created");
+ok(-d REV_DIR, "REV_DIR directory created");
+
+# Test 'add'
+open my $fh, '>', "test.txt" or die $!;
+print $fh "Hello, world!";
+close $fh;
+
+ok(system("perl $cmd add test.txt > /dev/null") == 0,
+ "Add command ran successfully");
+ok(-d TMP_TREE, "Staging tree created");
+
+# Test 'commit -m'
+ok(system("perl $cmd commit -m 'Initial commit' > /dev/null") == 0,
+ "Commit with -m ran successfully");
+my $rev1 = File::Spec->catdir(REV_DIR, "0000001");
+ok(-d $rev1, "Revision 0000001 created");
+
+# Check the message content
+my $msg_file = File::Spec->catfile($rev1, "message");
+ok(-f $msg_file, "Message file exists in revision 1");
+
+open my $msg_fh, '<', $msg_file or die $!;
+my $saved_msg = <$msg_fh>; chomp $saved_msg; close $msg_fh;
+is($saved_msg, "Initial commit", "Commit message matches the input");
+
+# Test 'commit -am' (Auto-staging)
+open my $fh2, '>>', "test.txt" or die $!;
+print $fh2 "\nMore content";
+close $fh2;
+
+ok(system("perl $cmd commit -am 'Second commit' > /dev/null") == 0,
+ "Commit -am (auto-staging) ran successfully");
+
+# Check if the object store updated
+opendir(my $dh, OBJ_DIR) or die $!;
+my @objs = grep { /[a-f0-9]{40}/ } readdir($dh);
+closedir($dh);
+
+ok(scalar @objs >= 1, "Objects exist in store");
+
+# Shuffle subdirectories
+make_path("dirA/dirB");
+write_file("dirA/dirB/shuffle.txt", "Same Content");
+system("perl $cmd add dirA/dirB/shuffle.txt > /dev/null");
+system("perl $cmd commit -m 'Shuffle part 1' > /dev/null");
+
+# Move and delete
+make_path("dirC");
+rename("dirA/dirB/shuffle.txt", "dirC/shuffle.txt");
+remove_tree("dirA");
+
+# Commit the move
+system("perl $cmd commit -am 'Shuffle part 2' > /dev/null");
+
+# Verify Revision 4's tree structure
+my $head = read_file(HEAD);
+my ($tree_ptr) = bsd_glob(File::Spec->catfile(REV_DIR, $head, "tree-*"));
+my ($tree_hash) = $tree_ptr =~ /tree-([a-f0-9]{40})$/;
+my $actual_tree_path = File::Spec->catdir(OBJ_DIR, $tree_hash);
+
+# Check for new path
+ok(-l File::Spec->catfile($actual_tree_path, "dirC/shuffle.txt"),
+ "New path dirC/shuffle.txt exists in object store");
+
+# Check for deleted path (Should NOT exist)
+ok(!-d File::Spec->catdir($actual_tree_path, "dirA"),
+ "Deleted directory removed from tree snapshot");
+
+# Verify the symlink still points to the correct content blob
+my $link_target = readlink(File::Spec->catfile($actual_tree_path, "dirC/shuffle.txt"));
+like($link_target, qr/obj\/[a-f0-9]{40}/, "Symlink in tree points to a valid object blob");
+
+## Symlink-to-symlink
+write_file("target.txt", "Final Destination");
+symlink("target.txt", "link_a");
+symlink("link_a", "link_b");
+
+system("perl $cmd add link_b > /dev/null");
+system("perl $cmd commit -m 'Double link' > /dev/null");
+
+my $head = read_file(HEAD);
+my ($tree_ptr) = bsd_glob(File::Spec->catfile(REV_DIR, $head, "tree-*"));
+my $staged_link = File::Spec->catfile(OBJ_DIR, $tree_ptr =~ s/.*tree-//r, "link_b");
+
+is(readlink($staged_link), "link_a", "Symlink-to-symlink preserved literal target");
+
+## Empty file
+write_file("empty.txt", "");
+system("perl $cmd add empty.txt > /dev/null");
+system("perl $cmd commit -m 'Empty file' > /dev/null");
+
+ok(-e File::Spec->catfile(OBJ_DIR, "da39a3ee5e6b4b0d3255bfef95601890afd80709"),
+ "Empty file object created (da39a3...)");
+
+# The overwrite
+write_file("repeat.txt", "Version A");
+system("perl $cmd add repeat.txt > /dev/null");
+system("perl $cmd commit -m 'State A' > /dev/null");
+my $tree_v1 = (bsd_glob(File::Spec->catfile(REV_DIR, read_file(HEAD), "tree-*")))[0];
+
+write_file("repeat.txt", "Version B");
+system("perl $cmd add repeat.txt > /dev/null");
+system("perl $cmd commit -m 'State B' > /dev/null");
+
+## Revert content
+write_file("repeat.txt", "Version A");
+system("perl $cmd add repeat.txt > /dev/null");
+system("perl $cmd commit -m 'Back to State A' > /dev/null");
+my $tree_v3 = (bsd_glob(File::Spec->catfile(REV_DIR, read_file(HEAD), "tree-*")))[0];
+
+is($tree_v1 =~ s/.*tree-//r, $tree_v3 =~ s/.*tree-//r,
+ "Tree hash reverted perfectly after content restoration");
+
+# Cleanup
+chdir($orig_dir);
+# remove_tree($sandbox); # Uncomment this once you're sure it works
+
+done_testing();
+
+sub write_file {
+ my ($path, $content) = @_;
+ open my $fh, '>', $path or die "Could not open '$path' for writing: $!";
+ print $fh $content;
+ close $fh or die "Could not close '$path' after writing: $!";
+}
+
+sub read_file {
+ my $path = shift;
+ open my $fh, '<', $path or return "";
+ my $content = <$fh>;
+ chomp $content if $content;
+ close $fh;
+ return $content;
+}
+
diff --git a/vcx b/vcx
index 11ba10d..e872961 100644
--- a/vcx
+++ b/vcx
@@ -2,7 +2,6 @@
use strict;
use warnings;
-
use File::Path qw(make_path);
use File::Copy qw(copy);
use File::Find;
@@ -15,9 +14,9 @@ use Digest::SHA qw(sha1_hex);
use POSIX qw(strftime);
use constant VCX_DIR => '.vcx';
-use constant HEAD => VCX_DIR . '/head'; # Current commit ID
-use constant OBJ_DIR => VCX_DIR . '/objs'; # Latest version of a file
-use constant REV_DIR => VCX_DIR . '/revs'; # Commits
+use constant HEAD => VCX_DIR . '/head'; # Current commit ID
+use constant OBJ_DIR => VCX_DIR . '/obj'; # Latest version of a file
+use constant REV_DIR => VCX_DIR . '/rev'; # Commits
# Staging area
use constant TMP_DIR => VCX_DIR . '/index';
@@ -160,10 +159,10 @@ sub run_add {
my $next_id_hex = to_hex_id(from_hex_id($head) + 1);
- my @entries;
+ my %entries;
open my $afh, '>>', TMP_META_FILE or die $!;
- init_stage($latest_tree_dir, \@entries);
+ init_stage($latest_tree_dir, \%entries);
foreach my $input (@targets) {
my @expanded = bsd_glob($input);
@@ -178,7 +177,7 @@ sub run_add {
return if -d $_;
my $rel = $File::Find::name =~ s|^\./||r;
- push @entries, $rel;
+ $entries{$rel} = 1;
my $staged_path = File::Spec->catfile(TMP_TREE, $rel);
my $prev_link = File::Spec->catfile($latest_tree_dir, $rel);
@@ -197,7 +196,7 @@ sub run_add {
my $obj_path = File::Spec->catfile(OBJ_DIR, $obj_name);
if (-e $obj_path) {
- my $p_path = File::Spec->catfile(TMP_DIFF, "$obj_name.$next_id_hex.patch");
+ my $p_path = File::Spec->catfile(TMP_DIFF, "$obj_name.patch");
if (-T $_) {
if (compare($_, $obj_path) != 0) {
unless (-d TMP_DIFF) { make_path(TMP_DIFF); }
@@ -231,9 +230,32 @@ sub run_add {
}, $t);
}
}
+
+ # 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 $tree_data = join("\n", sort @entries);
+ 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;
@@ -514,7 +536,7 @@ sub init_stage {
my $target = readlink($_);
symlink($target, $staged_path) or die "Failed to link $rel: $!";
- push @$entries_ref, $rel;
+ $entries_ref->{$rel} = 1;
},
no_chdir => 1
}, $latest_tree_dir);