diff options
| author | Sadeep Madurange <sadeep@asciimx.com> | 2026-04-04 11:55:08 +0800 |
|---|---|---|
| committer | Sadeep Madurange <sadeep@asciimx.com> | 2026-04-04 14:52:02 +0800 |
| commit | cf421aeff4faa15a2a80495345ae76023830ca86 (patch) | |
| tree | d417dd85f75fc4c532bb0954ed59bea87d350914 | |
| parent | 3af329abee56900dd42cb305bd7a431ab3ded90a (diff) | |
| download | cvn-cf421aeff4faa15a2a80495345ae76023830ca86.tar.gz | |
Unit tests and fix hash collisions, deletions.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 21 | ||||
| -rw-r--r-- | main.c | 508 | ||||
| -rw-r--r-- | test_vcx.t | 167 | ||||
| -rw-r--r-- | vcx | 42 |
5 files changed, 200 insertions, 539 deletions
@@ -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) @@ -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; +} + @@ -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); |
