authorgravatar for ian@ianjohnson.devIan Johnson <ian@ianjohnson.dev> 2024-11-18 22:59:10-05:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-11-19 00:35:00-08:00
logdceab4502abf7af6f1ce1a7fa5c9143a46ac8ffa
treee02e2cddb3fb0aa415bc6324966ea3834c237084
parentfbcb00fbb38715cede40fb4af6b6f99b771ff02a

zig fetch: handle redirects for Git packages

Closes #21976

2 files changed, 106 insertions(+), 71 deletions(-)

src/Package/Fetch.zig+14-19
......@@ -812,6 +812,7 @@ const Resource = union(enum) {
812812 dir: fs.Dir,
813813
814814 const Git = struct {
815 session: git.Session,
815816 fetch_stream: git.Session.FetchStream,
816817 want_oid: [git.oid_length]u8,
817818 };
......@@ -820,7 +821,10 @@ const Resource = union(enum) {
820821 switch (resource.*) {
821822 .file => |*file| file.close(),
822823 .http_request => |*req| req.deinit(),
823 .git => |*git_resource| git_resource.fetch_stream.deinit(),
824 .git => |*git_resource| {
825 git_resource.fetch_stream.deinit();
826 git_resource.session.deinit();
827 },
824828 .dir => |*dir| dir.close(),
825829 }
826830 resource.* = undefined;
......@@ -961,23 +965,13 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
961965 {
962966 var transport_uri = uri;
963967 transport_uri.scheme = uri.scheme["git+".len..];
964 var redirect_uri: []u8 = undefined;
965 var session: git.Session = .{ .transport = http_client, .uri = transport_uri };
966 session.discoverCapabilities(gpa, &redirect_uri, server_header_buffer) catch |err| switch (err) {
967 error.Redirected => {
968 defer gpa.free(redirect_uri);
969 return f.fail(f.location_tok, try eb.printString(
970 "repository moved to {s}",
971 .{redirect_uri},
972 ));
973 },
974 else => |e| {
975 return f.fail(f.location_tok, try eb.printString(
976 "unable to discover remote git server capabilities: {s}",
977 .{@errorName(e)},
978 ));
979 },
968 var session = git.Session.init(gpa, http_client, transport_uri, server_header_buffer) catch |err| {
969 return f.fail(f.location_tok, try eb.printString(
970 "unable to discover remote git server capabilities: {s}",
971 .{@errorName(err)},
972 ));
980973 };
974 errdefer session.deinit();
981975
982976 const want_oid = want_oid: {
983977 const want_ref =
......@@ -987,7 +981,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
987981 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
988982 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
989983
990 var ref_iterator = session.listRefs(gpa, .{
984 var ref_iterator = session.listRefs(.{
991985 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
992986 .include_peeled = true,
993987 .server_header_buffer = server_header_buffer,
......@@ -1035,7 +1029,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
10351029 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{
10361030 std.fmt.fmtSliceHexLower(&want_oid),
10371031 }) catch unreachable;
1038 var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}, server_header_buffer) catch |err| {
1032 var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| {
10391033 return f.fail(f.location_tok, try eb.printString(
10401034 "unable to create fetch stream: {s}",
10411035 .{@errorName(err)},
......@@ -1044,6 +1038,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
10441038 errdefer fetch_stream.deinit();
10451039
10461040 return .{ .git = .{
1041 .session = session,
10471042 .fetch_stream = fetch_stream,
10481043 .want_oid = want_oid,
10491044 } };
src/Package/Fetch/git.zig+92-52
......@@ -492,26 +492,31 @@ const Packet = union(enum) {
492492/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
493493pub const Session = struct {
494494 transport: *std.http.Client,
495 uri: std.Uri,
496 supports_agent: bool = false,
497 supports_shallow: bool = false,
495 location: Location,
496 supports_agent: bool,
497 supports_shallow: bool,
498 allocator: Allocator,
498499
499500 const agent = "zig/" ++ @import("builtin").zig_version_string;
500501 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
501502
502 /// Discovers server capabilities. This should be called before using any
503 /// other client functionality, or the client will be forced to default to
504 /// the bare minimum server requirements, which may be considerably less
505 /// efficient (e.g. no shallow fetches).
506 ///
507 /// See the note on `getCapabilities` regarding `redirect_uri`.
508 pub fn discoverCapabilities(
509 session: *Session,
503 /// Initializes a client session and discovers the capabilities of the
504 /// server for optimal transport.
505 pub fn init(
510506 allocator: Allocator,
511 redirect_uri: *[]u8,
507 transport: *std.http.Client,
508 uri: std.Uri,
512509 http_headers_buffer: []u8,
513 ) !void {
514 var capability_iterator = try session.getCapabilities(allocator, redirect_uri, http_headers_buffer);
510 ) !Session {
511 var session: Session = .{
512 .transport = transport,
513 .location = try .init(allocator, uri),
514 .supports_agent = false,
515 .supports_shallow = false,
516 .allocator = allocator,
517 };
518 errdefer session.deinit();
519 var capability_iterator = try session.getCapabilities(http_headers_buffer);
515520 defer capability_iterator.deinit();
516521 while (try capability_iterator.next()) |capability| {
517522 if (mem.eql(u8, capability.key, "agent")) {
......@@ -525,27 +530,63 @@ pub const Session = struct {
525530 }
526531 }
527532 }
533 return session;
528534 }
529535
536 pub fn deinit(session: *Session) void {
537 session.location.deinit(session.allocator);
538 session.* = undefined;
539 }
540
541 /// An owned `std.Uri` representing the location of the server (base URI).
542 const Location = struct {
543 uri: std.Uri,
544
545 fn init(allocator: Allocator, uri: std.Uri) !Location {
546 const scheme = try allocator.dupe(u8, uri.scheme);
547 errdefer allocator.free(scheme);
548 const user = if (uri.user) |user| try std.fmt.allocPrint(allocator, "{user}", .{user}) else null;
549 errdefer if (user) |s| allocator.free(s);
550 const password = if (uri.password) |password| try std.fmt.allocPrint(allocator, "{password}", .{password}) else null;
551 errdefer if (password) |s| allocator.free(s);
552 const host = if (uri.host) |host| try std.fmt.allocPrint(allocator, "{host}", .{host}) else null;
553 errdefer if (host) |s| allocator.free(s);
554 const path = try std.fmt.allocPrint(allocator, "{path}", .{uri.path});
555 errdefer allocator.free(path);
556 // The query and fragment are not used as part of the base server URI.
557 return .{
558 .uri = .{
559 .scheme = scheme,
560 .user = if (user) |s| .{ .percent_encoded = s } else null,
561 .password = if (password) |s| .{ .percent_encoded = s } else null,
562 .host = if (host) |s| .{ .percent_encoded = s } else null,
563 .port = uri.port,
564 .path = .{ .percent_encoded = path },
565 },
566 };
567 }
568
569 fn deinit(loc: *Location, allocator: Allocator) void {
570 allocator.free(loc.uri.scheme);
571 if (loc.uri.user) |user| allocator.free(user.percent_encoded);
572 if (loc.uri.password) |password| allocator.free(password.percent_encoded);
573 if (loc.uri.host) |host| allocator.free(host.percent_encoded);
574 allocator.free(loc.uri.path.percent_encoded);
575 }
576 };
577
530578 /// Returns an iterator over capabilities supported by the server.
531579 ///
532 /// If the server redirects the request, `error.Redirected` is returned and
533 /// `redirect_uri` is populated with the URI resulting from the redirects.
534 /// When this occurs, the value of `redirect_uri` must be freed with
535 /// `allocator` when the caller is done with it.
536 fn getCapabilities(
537 session: Session,
538 allocator: Allocator,
539 redirect_uri: *[]u8,
540 http_headers_buffer: []u8,
541 ) !CapabilityIterator {
542 var info_refs_uri = session.uri;
580 /// The `session.location` is updated if the server returns a redirect, so
581 /// that subsequent session functions do not need to handle redirects.
582 fn getCapabilities(session: *Session, http_headers_buffer: []u8) !CapabilityIterator {
583 var info_refs_uri = session.location.uri;
543584 {
544 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
545 defer allocator.free(session_uri_path);
546 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "info/refs" }) };
585 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
586 defer session.allocator.free(session_uri_path);
587 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "info/refs" }) };
547588 }
548 defer allocator.free(info_refs_uri.path.percent_encoded);
589 defer session.allocator.free(info_refs_uri.path.percent_encoded);
549590 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
550591 info_refs_uri.fragment = null;
551592
......@@ -565,14 +606,14 @@ pub const Session = struct {
565606 if (request.response.status != .ok) return error.ProtocolError;
566607 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
567608 if (any_redirects_occurred) {
568 const request_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{request.uri.path});
569 defer allocator.free(request_uri_path);
609 const request_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{request.uri.path});
610 defer session.allocator.free(request_uri_path);
570611 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
571612 var new_uri = request.uri;
572613 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
573 new_uri.query = null;
574 redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});
575 return error.Redirected;
614 const new_location: Location = try .init(session.allocator, new_uri);
615 session.location.deinit(session.allocator);
616 session.location = new_location;
576617 }
577618
578619 const reader = request.reader();
......@@ -649,28 +690,28 @@ pub const Session = struct {
649690 };
650691
651692 /// Returns an iterator over refs known to the server.
652 pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {
653 var upload_pack_uri = session.uri;
693 pub fn listRefs(session: Session, options: ListRefsOptions) !RefIterator {
694 var upload_pack_uri = session.location.uri;
654695 {
655 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
656 defer allocator.free(session_uri_path);
657 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
696 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
697 defer session.allocator.free(session_uri_path);
698 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
658699 }
659 defer allocator.free(upload_pack_uri.path.percent_encoded);
700 defer session.allocator.free(upload_pack_uri.path.percent_encoded);
660701 upload_pack_uri.query = null;
661702 upload_pack_uri.fragment = null;
662703
663704 var body: std.ArrayListUnmanaged(u8) = .empty;
664 defer body.deinit(allocator);
665 const body_writer = body.writer(allocator);
705 defer body.deinit(session.allocator);
706 const body_writer = body.writer(session.allocator);
666707 try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
667708 if (session.supports_agent) {
668709 try Packet.write(.{ .data = agent_capability }, body_writer);
669710 }
670711 try Packet.write(.delimiter, body_writer);
671712 for (options.ref_prefixes) |ref_prefix| {
672 const ref_prefix_packet = try std.fmt.allocPrint(allocator, "ref-prefix {s}\n", .{ref_prefix});
673 defer allocator.free(ref_prefix_packet);
713 const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix});
714 defer session.allocator.free(ref_prefix_packet);
674715 try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
675716 }
676717 if (options.include_symrefs) {
......@@ -753,23 +794,22 @@ pub const Session = struct {
753794 /// performed if the server supports it.
754795 pub fn fetch(
755796 session: Session,
756 allocator: Allocator,
757797 wants: []const []const u8,
758798 http_headers_buffer: []u8,
759799 ) !FetchStream {
760 var upload_pack_uri = session.uri;
800 var upload_pack_uri = session.location.uri;
761801 {
762 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
763 defer allocator.free(session_uri_path);
764 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
802 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
803 defer session.allocator.free(session_uri_path);
804 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
765805 }
766 defer allocator.free(upload_pack_uri.path.percent_encoded);
806 defer session.allocator.free(upload_pack_uri.path.percent_encoded);
767807 upload_pack_uri.query = null;
768808 upload_pack_uri.fragment = null;
769809
770810 var body: std.ArrayListUnmanaged(u8) = .empty;
771 defer body.deinit(allocator);
772 const body_writer = body.writer(allocator);
811 defer body.deinit(session.allocator);
812 const body_writer = body.writer(session.allocator);
773813 try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
774814 if (session.supports_agent) {
775815 try Packet.write(.{ .data = agent_capability }, body_writer);