Commit 573a13f8be

Ian Johnson <ian@ianjohnson.dev>
2023-10-03 02:59:00
Support symlinks for git+http(s) dependencies
1 parent 2118118
Changed files (2)
src/git.zig
@@ -38,6 +38,32 @@ test parseOid {
     try testing.expectError(error.InvalidOid, parseOid("HEAD"));
 }
 
+pub const Diagnostics = struct {
+    allocator: Allocator,
+    errors: std.ArrayListUnmanaged(Error) = .{},
+
+    pub const Error = union(enum) {
+        unable_to_create_sym_link: struct {
+            code: anyerror,
+            file_name: []const u8,
+            link_name: []const u8,
+        },
+    };
+
+    pub fn deinit(d: *Diagnostics) void {
+        for (d.errors.items) |item| {
+            switch (item) {
+                .unable_to_create_sym_link => |info| {
+                    d.allocator.free(info.file_name);
+                    d.allocator.free(info.link_name);
+                },
+            }
+        }
+        d.errors.deinit(d.allocator);
+        d.* = undefined;
+    }
+};
+
 pub const Repository = struct {
     odb: Odb,
 
@@ -55,6 +81,7 @@ pub const Repository = struct {
         repository: *Repository,
         worktree: std.fs.Dir,
         commit_oid: Oid,
+        diagnostics: *Diagnostics,
     ) !void {
         try repository.odb.seekOid(commit_oid);
         const tree_oid = tree_oid: {
@@ -62,7 +89,7 @@ pub const Repository = struct {
             if (commit_object.type != .commit) return error.NotACommit;
             break :tree_oid try getCommitTree(commit_object.data);
         };
-        try repository.checkoutTree(worktree, tree_oid);
+        try repository.checkoutTree(worktree, tree_oid, "", diagnostics);
     }
 
     /// Checks out the tree at `tree_oid` to `worktree`.
@@ -70,6 +97,8 @@ pub const Repository = struct {
         repository: *Repository,
         dir: std.fs.Dir,
         tree_oid: Oid,
+        current_path: []const u8,
+        diagnostics: *Diagnostics,
     ) !void {
         try repository.odb.seekOid(tree_oid);
         const tree_object = try repository.odb.readObject();
@@ -87,7 +116,9 @@ pub const Repository = struct {
                     try dir.makeDir(entry.name);
                     var subdir = try dir.openDir(entry.name, .{});
                     defer subdir.close();
-                    try repository.checkoutTree(subdir, entry.oid);
+                    const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
+                    defer repository.odb.allocator.free(sub_path);
+                    try repository.checkoutTree(subdir, entry.oid, sub_path, diagnostics);
                 },
                 .file => {
                     var file = try dir.createFile(entry.name, .{});
@@ -98,7 +129,23 @@ pub const Repository = struct {
                     try file.writeAll(file_object.data);
                     try file.sync();
                 },
-                .symlink => return error.SymlinkNotSupported,
+                .symlink => {
+                    try repository.odb.seekOid(entry.oid);
+                    var symlink_object = try repository.odb.readObject();
+                    if (symlink_object.type != .blob) return error.InvalidFile;
+                    const link_name = symlink_object.data;
+                    dir.symLink(link_name, entry.name, .{}) catch |e| {
+                        const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
+                        errdefer diagnostics.allocator.free(file_name);
+                        const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
+                        errdefer diagnostics.allocator.free(link_name_dup);
+                        try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
+                            .code = e,
+                            .file_name = file_name,
+                            .link_name = link_name_dup,
+                        } });
+                    };
+                },
                 .gitlink => {
                     // Consistent with git archive behavior, create the directory but
                     // do nothing else
src/Package.zig
@@ -778,7 +778,7 @@ pub const ReadableResource = struct {
                             .tar => try unpackTarball(allocator, prog_reader.reader(), tmp_directory.handle, dep_location_tok, report),
                             .@"tar.gz" => try unpackTarballCompressed(allocator, prog_reader, tmp_directory.handle, dep_location_tok, report, std.compress.gzip),
                             .@"tar.xz" => try unpackTarballCompressed(allocator, prog_reader, tmp_directory.handle, dep_location_tok, report, std.compress.xz),
-                            .git_pack => try unpackGitPack(allocator, &prog_reader, git.parseOid(rr.path) catch unreachable, tmp_directory.handle),
+                            .git_pack => try unpackGitPack(allocator, &prog_reader, git.parseOid(rr.path) catch unreachable, tmp_directory.handle, dep_location_tok, report),
                         }
                     } else {
                         // Recursive directory copy.
@@ -1220,6 +1220,8 @@ fn unpackGitPack(
     reader: anytype,
     want_oid: git.Oid,
     out_dir: fs.Dir,
+    dep_location_tok: std.zig.Ast.TokenIndex,
+    report: Report,
 ) !void {
     // The .git directory is used to store the packfile and associated index, but
     // we do not attempt to replicate the exact structure of a real .git
@@ -1251,7 +1253,32 @@ fn unpackGitPack(
             checkout_prog_node.activate();
             var repository = try git.Repository.init(gpa, pack_file, index_file);
             defer repository.deinit();
-            try repository.checkout(out_dir, want_oid);
+            var diagnostics: git.Diagnostics = .{ .allocator = gpa };
+            defer diagnostics.deinit();
+            try repository.checkout(out_dir, want_oid, &diagnostics);
+
+            if (diagnostics.errors.items.len > 0) {
+                const notes_len: u32 = @intCast(diagnostics.errors.items.len);
+                try report.addErrorWithNotes(notes_len, .{
+                    .tok = dep_location_tok,
+                    .off = 0,
+                    .msg = "unable to unpack packfile",
+                });
+                const eb = report.error_bundle;
+                const notes_start = try eb.reserveNotes(notes_len);
+                for (diagnostics.errors.items, notes_start..) |item, note_i| {
+                    switch (item) {
+                        .unable_to_create_sym_link => |info| {
+                            eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
+                                .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
+                                    info.file_name, info.link_name, @errorName(info.code),
+                                }),
+                            }));
+                        },
+                    }
+                }
+                return error.InvalidGitPack;
+            }
         }
     }