Commit dceab4502a

Ian Johnson <ian@ianjohnson.dev>
2024-11-19 04:59:10
zig fetch: handle redirects for Git packages
Closes #21976
1 parent fbcb00f
Changed files (2)
src
Package
src/Package/Fetch/git.zig
@@ -492,26 +492,31 @@ const Packet = union(enum) {
 /// [protocol-v2](https://git-scm.com/docs/protocol-v2).
 pub const Session = struct {
     transport: *std.http.Client,
-    uri: std.Uri,
-    supports_agent: bool = false,
-    supports_shallow: bool = false,
+    location: Location,
+    supports_agent: bool,
+    supports_shallow: bool,
+    allocator: Allocator,
 
     const agent = "zig/" ++ @import("builtin").zig_version_string;
     const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
 
-    /// Discovers server capabilities. This should be called before using any
-    /// other client functionality, or the client will be forced to default to
-    /// the bare minimum server requirements, which may be considerably less
-    /// efficient (e.g. no shallow fetches).
-    ///
-    /// See the note on `getCapabilities` regarding `redirect_uri`.
-    pub fn discoverCapabilities(
-        session: *Session,
+    /// Initializes a client session and discovers the capabilities of the
+    /// server for optimal transport.
+    pub fn init(
         allocator: Allocator,
-        redirect_uri: *[]u8,
+        transport: *std.http.Client,
+        uri: std.Uri,
         http_headers_buffer: []u8,
-    ) !void {
-        var capability_iterator = try session.getCapabilities(allocator, redirect_uri, http_headers_buffer);
+    ) !Session {
+        var session: Session = .{
+            .transport = transport,
+            .location = try .init(allocator, uri),
+            .supports_agent = false,
+            .supports_shallow = false,
+            .allocator = allocator,
+        };
+        errdefer session.deinit();
+        var capability_iterator = try session.getCapabilities(http_headers_buffer);
         defer capability_iterator.deinit();
         while (try capability_iterator.next()) |capability| {
             if (mem.eql(u8, capability.key, "agent")) {
@@ -525,27 +530,63 @@ pub const Session = struct {
                 }
             }
         }
+        return session;
     }
 
+    pub fn deinit(session: *Session) void {
+        session.location.deinit(session.allocator);
+        session.* = undefined;
+    }
+
+    /// An owned `std.Uri` representing the location of the server (base URI).
+    const Location = struct {
+        uri: std.Uri,
+
+        fn init(allocator: Allocator, uri: std.Uri) !Location {
+            const scheme = try allocator.dupe(u8, uri.scheme);
+            errdefer allocator.free(scheme);
+            const user = if (uri.user) |user| try std.fmt.allocPrint(allocator, "{user}", .{user}) else null;
+            errdefer if (user) |s| allocator.free(s);
+            const password = if (uri.password) |password| try std.fmt.allocPrint(allocator, "{password}", .{password}) else null;
+            errdefer if (password) |s| allocator.free(s);
+            const host = if (uri.host) |host| try std.fmt.allocPrint(allocator, "{host}", .{host}) else null;
+            errdefer if (host) |s| allocator.free(s);
+            const path = try std.fmt.allocPrint(allocator, "{path}", .{uri.path});
+            errdefer allocator.free(path);
+            // The query and fragment are not used as part of the base server URI.
+            return .{
+                .uri = .{
+                    .scheme = scheme,
+                    .user = if (user) |s| .{ .percent_encoded = s } else null,
+                    .password = if (password) |s| .{ .percent_encoded = s } else null,
+                    .host = if (host) |s| .{ .percent_encoded = s } else null,
+                    .port = uri.port,
+                    .path = .{ .percent_encoded = path },
+                },
+            };
+        }
+
+        fn deinit(loc: *Location, allocator: Allocator) void {
+            allocator.free(loc.uri.scheme);
+            if (loc.uri.user) |user| allocator.free(user.percent_encoded);
+            if (loc.uri.password) |password| allocator.free(password.percent_encoded);
+            if (loc.uri.host) |host| allocator.free(host.percent_encoded);
+            allocator.free(loc.uri.path.percent_encoded);
+        }
+    };
+
     /// Returns an iterator over capabilities supported by the server.
     ///
-    /// If the server redirects the request, `error.Redirected` is returned and
-    /// `redirect_uri` is populated with the URI resulting from the redirects.
-    /// When this occurs, the value of `redirect_uri` must be freed with
-    /// `allocator` when the caller is done with it.
-    fn getCapabilities(
-        session: Session,
-        allocator: Allocator,
-        redirect_uri: *[]u8,
-        http_headers_buffer: []u8,
-    ) !CapabilityIterator {
-        var info_refs_uri = session.uri;
+    /// The `session.location` is updated if the server returns a redirect, so
+    /// that subsequent session functions do not need to handle redirects.
+    fn getCapabilities(session: *Session, http_headers_buffer: []u8) !CapabilityIterator {
+        var info_refs_uri = session.location.uri;
         {
-            const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
-            defer allocator.free(session_uri_path);
-            info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "info/refs" }) };
+            const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
+            defer session.allocator.free(session_uri_path);
+            info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "info/refs" }) };
         }
-        defer allocator.free(info_refs_uri.path.percent_encoded);
+        defer session.allocator.free(info_refs_uri.path.percent_encoded);
         info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
         info_refs_uri.fragment = null;
 
@@ -565,14 +606,14 @@ pub const Session = struct {
         if (request.response.status != .ok) return error.ProtocolError;
         const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
         if (any_redirects_occurred) {
-            const request_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{request.uri.path});
-            defer allocator.free(request_uri_path);
+            const request_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{request.uri.path});
+            defer session.allocator.free(request_uri_path);
             if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
             var new_uri = request.uri;
             new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
-            new_uri.query = null;
-            redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});
-            return error.Redirected;
+            const new_location: Location = try .init(session.allocator, new_uri);
+            session.location.deinit(session.allocator);
+            session.location = new_location;
         }
 
         const reader = request.reader();
@@ -649,28 +690,28 @@ pub const Session = struct {
     };
 
     /// Returns an iterator over refs known to the server.
-    pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {
-        var upload_pack_uri = session.uri;
+    pub fn listRefs(session: Session, options: ListRefsOptions) !RefIterator {
+        var upload_pack_uri = session.location.uri;
         {
-            const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
-            defer allocator.free(session_uri_path);
-            upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
+            const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
+            defer session.allocator.free(session_uri_path);
+            upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
         }
-        defer allocator.free(upload_pack_uri.path.percent_encoded);
+        defer session.allocator.free(upload_pack_uri.path.percent_encoded);
         upload_pack_uri.query = null;
         upload_pack_uri.fragment = null;
 
         var body: std.ArrayListUnmanaged(u8) = .empty;
-        defer body.deinit(allocator);
-        const body_writer = body.writer(allocator);
+        defer body.deinit(session.allocator);
+        const body_writer = body.writer(session.allocator);
         try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
         if (session.supports_agent) {
             try Packet.write(.{ .data = agent_capability }, body_writer);
         }
         try Packet.write(.delimiter, body_writer);
         for (options.ref_prefixes) |ref_prefix| {
-            const ref_prefix_packet = try std.fmt.allocPrint(allocator, "ref-prefix {s}\n", .{ref_prefix});
-            defer allocator.free(ref_prefix_packet);
+            const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix});
+            defer session.allocator.free(ref_prefix_packet);
             try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
         }
         if (options.include_symrefs) {
@@ -753,23 +794,22 @@ pub const Session = struct {
     /// performed if the server supports it.
     pub fn fetch(
         session: Session,
-        allocator: Allocator,
         wants: []const []const u8,
         http_headers_buffer: []u8,
     ) !FetchStream {
-        var upload_pack_uri = session.uri;
+        var upload_pack_uri = session.location.uri;
         {
-            const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
-            defer allocator.free(session_uri_path);
-            upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
+            const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
+            defer session.allocator.free(session_uri_path);
+            upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
         }
-        defer allocator.free(upload_pack_uri.path.percent_encoded);
+        defer session.allocator.free(upload_pack_uri.path.percent_encoded);
         upload_pack_uri.query = null;
         upload_pack_uri.fragment = null;
 
         var body: std.ArrayListUnmanaged(u8) = .empty;
-        defer body.deinit(allocator);
-        const body_writer = body.writer(allocator);
+        defer body.deinit(session.allocator);
+        const body_writer = body.writer(session.allocator);
         try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
         if (session.supports_agent) {
             try Packet.write(.{ .data = agent_capability }, body_writer);
src/Package/Fetch.zig
@@ -812,6 +812,7 @@ const Resource = union(enum) {
     dir: fs.Dir,
 
     const Git = struct {
+        session: git.Session,
         fetch_stream: git.Session.FetchStream,
         want_oid: [git.oid_length]u8,
     };
@@ -820,7 +821,10 @@ const Resource = union(enum) {
         switch (resource.*) {
             .file => |*file| file.close(),
             .http_request => |*req| req.deinit(),
-            .git => |*git_resource| git_resource.fetch_stream.deinit(),
+            .git => |*git_resource| {
+                git_resource.fetch_stream.deinit();
+                git_resource.session.deinit();
+            },
             .dir => |*dir| dir.close(),
         }
         resource.* = undefined;
@@ -961,23 +965,13 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
     {
         var transport_uri = uri;
         transport_uri.scheme = uri.scheme["git+".len..];
-        var redirect_uri: []u8 = undefined;
-        var session: git.Session = .{ .transport = http_client, .uri = transport_uri };
-        session.discoverCapabilities(gpa, &redirect_uri, server_header_buffer) catch |err| switch (err) {
-            error.Redirected => {
-                defer gpa.free(redirect_uri);
-                return f.fail(f.location_tok, try eb.printString(
-                    "repository moved to {s}",
-                    .{redirect_uri},
-                ));
-            },
-            else => |e| {
-                return f.fail(f.location_tok, try eb.printString(
-                    "unable to discover remote git server capabilities: {s}",
-                    .{@errorName(e)},
-                ));
-            },
+        var session = git.Session.init(gpa, http_client, transport_uri, server_header_buffer) catch |err| {
+            return f.fail(f.location_tok, try eb.printString(
+                "unable to discover remote git server capabilities: {s}",
+                .{@errorName(err)},
+            ));
         };
+        errdefer session.deinit();
 
         const want_oid = want_oid: {
             const want_ref =
@@ -987,7 +981,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
             const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
             const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
 
-            var ref_iterator = session.listRefs(gpa, .{
+            var ref_iterator = session.listRefs(.{
                 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
                 .include_peeled = true,
                 .server_header_buffer = server_header_buffer,
@@ -1035,7 +1029,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
         _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{
             std.fmt.fmtSliceHexLower(&want_oid),
         }) catch unreachable;
-        var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}, server_header_buffer) catch |err| {
+        var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| {
             return f.fail(f.location_tok, try eb.printString(
                 "unable to create fetch stream: {s}",
                 .{@errorName(err)},
@@ -1044,6 +1038,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
         errdefer fetch_stream.deinit();
 
         return .{ .git = .{
+            .session = session,
             .fetch_stream = fetch_stream,
             .want_oid = want_oid,
         } };