Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

Session

A client session for the Git protocol, currently limited to an HTTP(S) transport. Only protocol version 2 is supported, as documented in protocol-v2.

git.Session
pub const Session = struct

File

Code

pub const Session = struct {
    transport: *std.http.Client,
    location: Location,
    supports_agent: bool,
    supports_shallow: bool,
    object_format: Oid.Format,
    arena: Allocator,

    const agent = "zig/" ++ @import("builtin").zig_version_string;
    const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});

    /// Initializes a client session and discovers the capabilities of the
    /// server for optimal transport.
    pub fn init(
        arena: Allocator,
        transport: *std.http.Client,
        uri: std.Uri,
        /// Asserted to be at least `Packet.max_data_length`
        response_buffer: []u8,
    ) !Session {
        assert(response_buffer.len >= Packet.max_data_length);
        var session: Session = .{
            .transport = transport,
            .location = try .init(arena, uri),
            .supports_agent = false,
            .supports_shallow = false,
            .object_format = .sha1,
            .arena = arena,
        };
        var capability_iterator: CapabilityIterator = undefined;
        try session.getCapabilities(&capability_iterator, response_buffer);
        defer capability_iterator.deinit();
        while (try capability_iterator.next()) |capability| {
            if (mem.eql(u8, capability.key, "agent")) {
                session.supports_agent = true;
            } else if (mem.eql(u8, capability.key, "fetch")) {
                var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
                while (feature_iterator.next()) |feature| {
                    if (mem.eql(u8, feature, "shallow")) {
                        session.supports_shallow = true;
                    }
                }
            } else if (mem.eql(u8, capability.key, "object-format")) {
                if (std.meta.stringToEnum(Oid.Format, capability.value orelse continue)) |format| {
                    session.object_format = format;
                }
            }
        }
        return session;
    }

    /// An owned `std.Uri` representing the location of the server (base URI).
    const Location = struct {
        uri: std.Uri,

        fn init(arena: Allocator, uri: std.Uri) !Location {
            const scheme = try arena.dupe(u8, uri.scheme);
            const user = if (uri.user) |user| try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(user, .formatUser),
            }) else null;
            const password = if (uri.password) |password| try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(password, .formatPassword),
            }) else null;
            const host = if (uri.host) |host| try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(host, .formatHost),
            }) else null;
            const path = try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(uri.path, .formatPath),
            });
            // 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 },
                },
            };
        }
    };

    /// Returns an iterator over capabilities supported by the server.
    ///
    /// 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, it: *CapabilityIterator, response_buffer: []u8) !void {
        const arena = session.arena;
        assert(response_buffer.len >= Packet.max_data_length);
        var info_refs_uri = session.location.uri;
        {
            const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(session.location.uri.path, .formatPath),
            });
            info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{
                "/", session_uri_path, "info/refs",
            }) };
        }
        info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
        info_refs_uri.fragment = null;

        const max_redirects = 3;
        it.* = .{
            .request = try session.transport.request(.GET, info_refs_uri, .{
                .redirect_behavior = .init(max_redirects),
                .extra_headers = &.{
                    .{ .name = "Git-Protocol", .value = "version=2" },
                },
            }),
            .reader = undefined,
            .decompress = undefined,
        };
        errdefer it.deinit();
        const request = &it.request;
        try request.sendBodiless();

        var redirect_buffer: [1024]u8 = undefined;
        var response = try request.receiveHead(&redirect_buffer);
        if (response.head.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(arena, "{f}", .{
                std.fmt.alt(request.uri.path, .formatPath),
            });
            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] };
            session.location = try .init(arena, new_uri);
        }

        const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
        it.reader = response.readerDecompressing(response_buffer, &it.decompress, decompress_buffer);
        var state: enum { response_start, response_content } = .response_start;
        while (true) {
            // Some Git servers (at least GitHub) include an additional
            // '# service=git-upload-pack' informative response before sending
            // the expected 'version 2' packet and capability information.
            // This is not universal: SourceHut, for example, does not do this.
            // Thus, we need to skip any such useless additional responses
            // before we get the one we're actually looking for. The responses
            // will be delimited by flush packets.
            const packet = Packet.read(it.reader) catch |err| switch (err) {
                error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
                else => |e| return e,
            };
            switch (packet) {
                .flush => state = .response_start,
                .data => |data| switch (state) {
                    .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) {
                        return;
                    } else {
                        state = .response_content;
                    },
                    else => {},
                },
                else => return error.UnexpectedPacket,
            }
        }
    }

    const CapabilityIterator = struct {
        request: std.http.Client.Request,
        reader: *Io.Reader,
        decompress: std.http.Decompress,

        const Capability = struct {
            key: []const u8,
            value: ?[]const u8 = null,

            fn parse(data: []const u8) Capability {
                return if (mem.indexOfScalar(u8, data, '=')) |separator_pos|
                    .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 ..] }
                else
                    .{ .key = data };
            }
        };

        fn deinit(it: *CapabilityIterator) void {
            it.request.deinit();
            it.* = undefined;
        }

        fn next(it: *CapabilityIterator) !?Capability {
            switch (try Packet.read(it.reader)) {
                .flush => return null,
                .data => |data| return Capability.parse(Packet.normalizeText(data)),
                else => return error.UnexpectedPacket,
            }
        }
    };

    const ListRefsOptions = struct {
        /// The ref prefixes (if any) to use to filter the refs available on the
        /// server. Note that the client must still check the returned refs
        /// against its desired filters itself: the server is not required to
        /// respect these prefix filters and may return other refs as well.
        ref_prefixes: []const []const u8 = &.{},
        /// Whether to include symref targets for returned symbolic refs.
        include_symrefs: bool = false,
        /// Whether to include the peeled object ID for returned tag refs.
        include_peeled: bool = false,
        /// Asserted to be at least `Packet.max_data_length`.
        buffer: []u8,
    };

    /// Returns an iterator over refs known to the server.
    pub fn listRefs(session: Session, it: *RefIterator, options: ListRefsOptions) !void {
        const arena = session.arena;
        assert(options.buffer.len >= Packet.max_data_length);
        var upload_pack_uri = session.location.uri;
        {
            const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(session.location.uri.path, .formatPath),
            });
            upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{ "/", session_uri_path, "git-upload-pack" }) };
        }
        upload_pack_uri.query = null;
        upload_pack_uri.fragment = null;

        var body: Io.Writer = .fixed(options.buffer);
        try Packet.write(.{ .data = "command=ls-refs\n" }, &body);
        if (session.supports_agent) {
            try Packet.write(.{ .data = agent_capability }, &body);
        }
        {
            const object_format_packet = try std.fmt.allocPrint(arena, "object-format={t}\n", .{
                session.object_format,
            });
            try Packet.write(.{ .data = object_format_packet }, &body);
        }
        try Packet.write(.delimiter, &body);
        for (options.ref_prefixes) |ref_prefix| {
            const ref_prefix_packet = try std.fmt.allocPrint(arena, "ref-prefix {s}\n", .{ref_prefix});
            try Packet.write(.{ .data = ref_prefix_packet }, &body);
        }
        if (options.include_symrefs) {
            try Packet.write(.{ .data = "symrefs\n" }, &body);
        }
        if (options.include_peeled) {
            try Packet.write(.{ .data = "peel\n" }, &body);
        }
        try Packet.write(.flush, &body);

        it.* = .{
            .request = try session.transport.request(.POST, upload_pack_uri, .{
                .redirect_behavior = .unhandled,
                .extra_headers = &.{
                    .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
                    .{ .name = "Git-Protocol", .value = "version=2" },
                },
            }),
            .reader = undefined,
            .format = session.object_format,
            .decompress = undefined,
        };
        const request = &it.request;
        errdefer request.deinit();
        try request.sendBodyComplete(body.buffered());

        var response = try request.receiveHead(options.buffer);
        if (response.head.status != .ok) return error.ProtocolError;
        const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
        it.reader = response.readerDecompressing(options.buffer, &it.decompress, decompress_buffer);
    }

    pub const RefIterator = struct {
        format: Oid.Format,
        request: std.http.Client.Request,
        reader: *Io.Reader,
        decompress: std.http.Decompress,

        pub const Ref = struct {
            oid: Oid,
            name: []const u8,
            symref_target: ?[]const u8,
            peeled: ?Oid,
        };

        pub fn deinit(iterator: *RefIterator) void {
            iterator.request.deinit();
            iterator.* = undefined;
        }

        pub fn next(it: *RefIterator) !?Ref {
            switch (try Packet.read(it.reader)) {
                .flush => return null,
                .data => |data| {
                    const ref_data = Packet.normalizeText(data);
                    const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
                    const oid = Oid.parse(it.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;

                    const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
                    const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];

                    var symref_target: ?[]const u8 = null;
                    var peeled: ?Oid = null;
                    var last_sep_pos = name_sep_pos;
                    while (last_sep_pos < ref_data.len) {
                        const next_sep_pos = mem.indexOfScalarPos(u8, ref_data, last_sep_pos + 1, ' ') orelse ref_data.len;
                        const attribute = ref_data[last_sep_pos + 1 .. next_sep_pos];
                        if (mem.startsWith(u8, attribute, "symref-target:")) {
                            symref_target = attribute["symref-target:".len..];
                        } else if (mem.startsWith(u8, attribute, "peeled:")) {
                            peeled = Oid.parse(it.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
                        }
                        last_sep_pos = next_sep_pos;
                    }

                    return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
                },
                else => return error.UnexpectedPacket,
            }
        }
    };

    /// Fetches the given refs from the server. A shallow fetch (depth 1) is
    /// performed if the server supports it.
    pub fn fetch(
        session: Session,
        fs: *FetchStream,
        wants: []const []const u8,
        /// Asserted to be at least `Packet.max_data_length`.
        response_buffer: []u8,
    ) !void {
        const arena = session.arena;
        assert(response_buffer.len >= Packet.max_data_length);
        var upload_pack_uri = session.location.uri;
        {
            const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
                std.fmt.alt(session.location.uri.path, .formatPath),
            });
            upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{ "/", session_uri_path, "git-upload-pack" }) };
        }
        upload_pack_uri.query = null;
        upload_pack_uri.fragment = null;

        var body: Io.Writer = .fixed(response_buffer);
        try Packet.write(.{ .data = "command=fetch\n" }, &body);
        if (session.supports_agent) {
            try Packet.write(.{ .data = agent_capability }, &body);
        }
        {
            const object_format_packet = try std.fmt.allocPrint(arena, "object-format={s}\n", .{@tagName(session.object_format)});
            try Packet.write(.{ .data = object_format_packet }, &body);
        }
        try Packet.write(.delimiter, &body);
        // Our packfile parser supports the OFS_DELTA object type
        try Packet.write(.{ .data = "ofs-delta\n" }, &body);
        // We do not currently convey server progress information to the user
        try Packet.write(.{ .data = "no-progress\n" }, &body);
        if (session.supports_shallow) {
            try Packet.write(.{ .data = "deepen 1\n" }, &body);
        }
        for (wants) |want| {
            var buf: [Packet.max_data_length]u8 = undefined;
            const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
            try Packet.write(.{ .data = arg }, &body);
        }
        try Packet.write(.{ .data = "done\n" }, &body);
        try Packet.write(.flush, &body);

        fs.* = .{
            .request = try session.transport.request(.POST, upload_pack_uri, .{
                .redirect_behavior = .not_allowed,
                .extra_headers = &.{
                    .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
                    .{ .name = "Git-Protocol", .value = "version=2" },
                },
            }),
            .input = undefined,
            .reader = undefined,
            .remaining_len = undefined,
            .decompress = undefined,
        };
        const request = &fs.request;
        errdefer request.deinit();

        try request.sendBodyComplete(body.buffered());

        var response = try request.receiveHead(&.{});
        if (response.head.status != .ok) return error.ProtocolError;

        const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
        const reader = response.readerDecompressing(response_buffer, &fs.decompress, decompress_buffer);
        // We are not interested in any of the sections of the returned fetch
        // data other than the packfile section, since we aren't doing anything
        // complex like ref negotiation (this is a fresh clone).
        var state: enum { section_start, section_content } = .section_start;
        while (true) {
            const packet = try Packet.read(reader);
            switch (state) {
                .section_start => switch (packet) {
                    .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) {
                        fs.input = reader;
                        fs.reader = .{
                            .buffer = &.{},
                            .vtable = &.{ .stream = FetchStream.stream },
                            .seek = 0,
                            .end = 0,
                        };
                        fs.remaining_len = 0;
                        return;
                    } else {
                        state = .section_content;
                    },
                    else => return error.UnexpectedPacket,
                },
                .section_content => switch (packet) {
                    .delimiter => state = .section_start,
                    .data => {},
                    else => return error.UnexpectedPacket,
                },
            }
        }
    }

    pub const FetchStream = struct {
        request: std.http.Client.Request,
        input: *Io.Reader,
        reader: Io.Reader,
        err: ?Error = null,
        remaining_len: usize,
        decompress: std.http.Decompress,

        pub fn deinit(fs: *FetchStream) void {
            fs.request.deinit();
        }

        pub const Error = error{
            InvalidPacket,
            ProtocolError,
            UnexpectedPacket,
            WriteFailed,
            ReadFailed,
            EndOfStream,
        };

        const StreamCode = enum(u8) {
            pack_data = 1,
            progress = 2,
            fatal_error = 3,
            _,
        };

        pub fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
            const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r));
            const input = fs.input;
            if (fs.remaining_len == 0) {
                while (true) {
                    switch (Packet.peek(input) catch |err| {
                        fs.err = err;
                        return error.ReadFailed;
                    }) {
                        .flush => return error.EndOfStream,
                        .data => |data| switch (@as(StreamCode, @fromBackingInt(@intCast(data[0])))) {
                            .pack_data => {
                                input.toss(1);
                                fs.remaining_len = data.len - 1;
                                break;
                            },
                            .fatal_error => {
                                fs.err = error.ProtocolError;
                                return error.ReadFailed;
                            },
                            else => {
                                input.toss(data.len);
                            },
                        },
                        else => {
                            fs.err = error.UnexpectedPacket;
                            return error.ReadFailed;
                        },
                    }
                }
            }
            const buf = limit.slice(try w.writableSliceGreedy(1));
            const n = @min(buf.len, fs.remaining_len);
            try input.readSliceAll(buf[0..n]);
            w.advance(n);
            fs.remaining_len -= n;
            return n;
        }
    };
}