├── .gitattributes ├── .gitignore ├── src ├── transport.zig ├── internal │ ├── c.zig │ ├── internal.zig │ └── make_c_option.zig ├── net.zig ├── odb.zig ├── refdb.zig ├── oidarray.zig ├── annotated_commit.zig ├── submodule.zig ├── writestream.zig ├── message.zig ├── hashsig.zig ├── strarray.zig ├── buffer.zig ├── describe.zig ├── status_list.zig ├── alloc.zig ├── notes.zig ├── proxy.zig ├── signature.zig ├── indexer.zig ├── mailmap.zig ├── reference.zig ├── tag.zig ├── refspec.zig ├── transaction.zig ├── attribute.zig ├── certificate.zig ├── errors.zig ├── reflog.zig ├── worktree.zig ├── pathspec.zig ├── oid.zig ├── merge.zig ├── object.zig ├── blame.zig ├── blob.zig ├── filter.zig ├── credential.zig ├── git.zig ├── patch.zig ├── commit.zig └── rebase.zig ├── TODO.md ├── .github └── workflows │ └── main.yml ├── sample.zig ├── LICENSE ├── .vscode └── tasks.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /src/transport.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const git = @import("git.zig"); 4 | 5 | pub const Transport = opaque { 6 | comptime { 7 | std.testing.refAllDecls(@This()); 8 | } 9 | }; 10 | 11 | comptime { 12 | std.testing.refAllDecls(@This()); 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/c.zig: -------------------------------------------------------------------------------- 1 | pub usingnamespace @cImport({ 2 | @cInclude("git2/sys/alloc.h"); 3 | @cInclude("git2/sys/credential.h"); 4 | @cInclude("git2/sys/diff.h"); 5 | @cInclude("git2/sys/hashsig.h"); 6 | @cInclude("git2/sys/path.h"); 7 | @cInclude("git2/sys/repository.h"); 8 | @cInclude("git2.h"); 9 | }); 10 | -------------------------------------------------------------------------------- /src/net.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const git = @import("git.zig"); 4 | 5 | /// Direction of the connection. 6 | /// 7 | /// We need this because we need to know whether we should call git-upload-pack or git-receive-pack on the remote 8 | /// end when get_refs gets called. 9 | pub const Direction = enum(c_uint) { 10 | fetch = 0, 11 | push = 1, 12 | }; 13 | 14 | comptime { 15 | std.testing.refAllDecls(@This()); 16 | } 17 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Plans 2 | 3 | - [ ] Implement entire API from commit `3f02b5b95e90d6be25aedf3f861e173182e815d7` 4 | - [ ] Update with all API changes up to latest master 5 | - [ ] Build against each version from latest to oldest and mark added APIs using `@hasDecl` 6 | - [ ] Add documentation to *every* function and type including docs for return value 7 | - [ ] Remove unnecessary duplicated named, e.g. `Credential.CredentialUsername` should be `Credential.Username` 8 | - [ ] Add functionality to build.zig to include as either static or dynamic 9 | 10 | 559 odb.h 11 | 606 merge.h 12 | 663 submodule.h 13 | 771 refs.h 14 | 1525 diff.h 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # push: 5 | # paths: 6 | # - '**.zig' 7 | # pull_request: 8 | # paths: 9 | # - '**.zig' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: goto-bus-stop/setup-zig@v1.3.0 18 | with: 19 | version: master 20 | 21 | - name: Install libgit2 22 | run: sudo apt install libgit2-dev 23 | 24 | - name: test 25 | run: zig build test -Dlibrary_version=pre_1.0 26 | lint: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: goto-bus-stop/setup-zig@v1.3.0 31 | with: 32 | version: master 33 | - run: zig fmt --check --ast-check **.zig 34 | -------------------------------------------------------------------------------- /sample.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const git = @import("git"); 3 | 4 | const repo_path = "./zig-cache/test_repo"; 5 | 6 | pub const libgit2_trace_log = true; 7 | 8 | pub fn main() !void { 9 | defer std.fs.cwd().deleteTree(repo_path) catch {}; 10 | 11 | const handle = try git.init(); 12 | defer handle.deinit(); 13 | 14 | const repo = try handle.repositoryInitExtended(repo_path, .{ .flags = .{ .mkdir = true, .mkpath = true } }); 15 | defer repo.deinit(); 16 | 17 | var git_buf = try handle.repositoryDiscover(repo_path, false, null); 18 | defer git_buf.deinit(); 19 | std.log.info("found repo @ {s}", .{git_buf.toSlice()}); 20 | 21 | const t = try handle.optionGetSearchPath(.system); 22 | std.log.info("search path: {s}", .{t.toSlice()}); 23 | } 24 | 25 | comptime { 26 | std.testing.refAllDecls(@This()); 27 | } 28 | -------------------------------------------------------------------------------- /src/odb.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Odb = opaque { 9 | pub fn deinit(self: *Odb) void { 10 | if (internal.trace_log) log.debug("Odb.deinit called", .{}); 11 | 12 | c.git_odb_free(@as(*c.git_odb, @ptrCast(self))); 13 | } 14 | 15 | pub fn repositoryOpen(self: *Odb) !*git.Repository { 16 | if (internal.trace_log) log.debug("Odb.repositoryOpen called", .{}); 17 | 18 | var repo: *git.Repository = undefined; 19 | 20 | try internal.wrapCall("git_repository_wrap_odb", .{ 21 | @as(*?*c.git_repository, @ptrCast(&repo)), 22 | @as(*c.git_odb, @ptrCast(self)), 23 | }); 24 | 25 | return repo; 26 | } 27 | 28 | comptime { 29 | std.testing.refAllDecls(@This()); 30 | } 31 | }; 32 | 33 | comptime { 34 | std.testing.refAllDecls(@This()); 35 | } 36 | -------------------------------------------------------------------------------- /src/refdb.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Refdb = opaque { 9 | pub fn deinit(self: *Refdb) void { 10 | if (internal.trace_log) log.debug("Refdb.deinit called", .{}); 11 | 12 | c.git_refdb_free(@as(*c.git_refdb, @ptrCast(self))); 13 | } 14 | 15 | /// Suggests that the given refdb compress or optimize its references. 16 | /// 17 | /// This mechanism is implementation specific. For on-disk reference databases, for example, this may pack all loose 18 | /// references. 19 | pub fn compress(self: *Refdb) !void { 20 | if (internal.trace_log) log.debug("Refdb.compress called", .{}); 21 | 22 | try internal.wrapCall("git_refdb_compress", .{ 23 | @as(*c.git_refdb, @ptrCast(self)), 24 | }); 25 | } 26 | 27 | comptime { 28 | std.testing.refAllDecls(@This()); 29 | } 30 | }; 31 | 32 | comptime { 33 | std.testing.refAllDecls(@This()); 34 | } 35 | -------------------------------------------------------------------------------- /src/oidarray.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Array of object ids 9 | pub const OidArray = extern struct { 10 | ids: [*]git.Oid, 11 | count: usize, 12 | 13 | /// Free the OID array 14 | /// 15 | /// This method must (and must only) be called on `OidArray` objects where the array is allocated by the library. 16 | /// Not doing so, will result in a memory leak. 17 | pub fn deinit(self: *OidArray) void { 18 | if (internal.trace_log) log.debug("OidArray.deinit called", .{}); 19 | 20 | c.git_oidarray_free(@as(*c.git_oidarray, @ptrCast(self))); 21 | } 22 | 23 | test { 24 | try std.testing.expectEqual(@sizeOf(c.git_oidarray), @sizeOf(OidArray)); 25 | try std.testing.expectEqual(@bitSizeOf(c.git_oidarray), @bitSizeOf(OidArray)); 26 | } 27 | 28 | comptime { 29 | std.testing.refAllDecls(@This()); 30 | } 31 | }; 32 | 33 | comptime { 34 | std.testing.refAllDecls(@This()); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Lee Cannon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "zig build", 8 | "type": "process", 9 | "command": "zig", 10 | "args": [ 11 | "build" 12 | ], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | }, 17 | "presentation": { 18 | "reveal": "silent", 19 | "showReuseMessage": false, 20 | "clear": true, 21 | "revealProblems": "onProblem" 22 | }, 23 | "problemMatcher": { 24 | "applyTo": "allDocuments", 25 | "fileLocation": "autoDetect", 26 | "owner": "zig", 27 | "pattern": { 28 | "regexp": "^(.*?):(\\d+):(\\d+):.*?(error):?\\s+(.*)$", 29 | "file": 1, 30 | "line": 2, 31 | "column": 3, 32 | "severity": 4, 33 | "message": 5 34 | }, 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/annotated_commit.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const AnnotatedCommit = opaque { 9 | pub fn deinit(self: *AnnotatedCommit) void { 10 | if (internal.trace_log) log.debug("AnnotatedCommit.deinit called", .{}); 11 | 12 | c.git_annotated_commit_free(@ptrCast(self)); 13 | } 14 | 15 | /// Gets the commit ID that the given `AnnotatedCommit` refers to. 16 | pub fn commitId(self: *AnnotatedCommit) !*const git.Oid { 17 | if (internal.trace_log) log.debug("AnnotatedCommit.commitId called", .{}); 18 | 19 | return @as( 20 | *const git.Oid, 21 | @ptrCast(c.git_annotated_commit_id(@ptrCast(self))), 22 | ); 23 | } 24 | 25 | /// Gets the refname that the given `AnnotatedCommit` refers to. 26 | pub fn refname(self: *AnnotatedCommit) ![:0]const u8 { 27 | if (internal.trace_log) log.debug("AnnotatedCommit.refname called", .{}); 28 | 29 | return std.mem.sliceTo( 30 | c.git_annotated_commit_ref(@ptrCast(self)), 31 | 0, 32 | ); 33 | } 34 | 35 | comptime { 36 | std.testing.refAllDecls(@This()); 37 | } 38 | }; 39 | 40 | comptime { 41 | std.testing.refAllDecls(@This()); 42 | } 43 | -------------------------------------------------------------------------------- /src/submodule.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Submodule ignore values 9 | /// 10 | /// These values represent settings for the `submodule.$name.ignore` configuration value which says how deeply to look at the 11 | /// working directory when getting submodule status. 12 | /// 13 | /// You can override this value in memory on a per-submodule basis with `Submodule.getIgnore` and can write the changed value to 14 | /// disk with `Submodule.save`. 15 | pub const SubmoduleIgnore = enum(c_int) { 16 | /// Use the submodule's configuration 17 | unspecified = -1, 18 | 19 | default = 0, 20 | 21 | /// Don't ignore any change - i.e. even an untracked file, will mark the submodule as dirty. Ignored files are still ignored, 22 | /// of course. 23 | none = 1, 24 | 25 | /// Ignore untracked files; only changes to tracked files, or the index or the HEAD commit will matter. 26 | untracked = 2, 27 | 28 | /// Ignore changes in the working directory, only considering changes if the HEAD of submodule has moved from the value in the 29 | /// superproject. 30 | dirty = 3, 31 | 32 | /// Never check if the submodule is dirty 33 | all = 4, 34 | }; 35 | 36 | comptime { 37 | std.testing.refAllDecls(@This()); 38 | } 39 | -------------------------------------------------------------------------------- /src/writestream.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const WriteStream = extern struct { 9 | /// if this returns non-zero this will be counted as an error 10 | write: *const fn (self: *WriteStream, buffer: [*:0]const u8, len: usize) callconv(.C) c_int, 11 | /// if this returns non-zero this will be counted as an error 12 | close: *const fn (self: *WriteStream) callconv(.C) c_int, 13 | free: *const fn (self: *WriteStream) callconv(.C) void, 14 | 15 | pub fn commit(self: *WriteStream) !git.Oid { 16 | if (internal.trace_log) log.debug("WriteStream.commit called", .{}); 17 | 18 | var ret: git.Oid = undefined; 19 | 20 | try internal.wrapCall("git_blob_create_from_stream_commit", .{ 21 | @as(*c.git_oid, @ptrCast(&ret)), 22 | @as(*c.git_writestream, @ptrCast(self)), 23 | }); 24 | 25 | return ret; 26 | } 27 | 28 | test { 29 | try std.testing.expectEqual(@sizeOf(c.git_writestream), @sizeOf(WriteStream)); 30 | try std.testing.expectEqual(@bitSizeOf(c.git_writestream), @bitSizeOf(WriteStream)); 31 | } 32 | 33 | comptime { 34 | std.testing.refAllDecls(@This()); 35 | } 36 | }; 37 | 38 | comptime { 39 | std.testing.refAllDecls(@This()); 40 | } 41 | -------------------------------------------------------------------------------- /src/message.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Represents an array of git message trailers. 9 | pub const MessageTrailerArray = extern struct { 10 | trailers: [*]MessageTrailer, 11 | count: usize, 12 | 13 | /// private 14 | _trailer_block: *u8, 15 | 16 | pub fn getTrailers(self: MessageTrailerArray) []MessageTrailer { 17 | return self.trailers[0..self.count]; 18 | } 19 | 20 | pub fn deinit(self: *MessageTrailerArray) void { 21 | if (internal.trace_log) log.debug("MessageTrailerArray.deinit called", .{}); 22 | 23 | c.git_message_trailer_array_free(@as(*c.git_message_trailer_array, @ptrCast(self))); 24 | } 25 | 26 | /// Represents a single git message trailer. 27 | pub const MessageTrailer = extern struct { 28 | key: [*:0]const u8, 29 | value: [*:0]const u8, 30 | 31 | test { 32 | try std.testing.expectEqual(@sizeOf(c.git_message_trailer), @sizeOf(MessageTrailer)); 33 | try std.testing.expectEqual(@bitSizeOf(c.git_message_trailer), @bitSizeOf(MessageTrailer)); 34 | } 35 | 36 | comptime { 37 | std.testing.refAllDecls(@This()); 38 | } 39 | }; 40 | 41 | test { 42 | try std.testing.expectEqual(@sizeOf(c.git_message_trailer_array), @sizeOf(MessageTrailerArray)); 43 | try std.testing.expectEqual(@bitSizeOf(c.git_message_trailer_array), @bitSizeOf(MessageTrailerArray)); 44 | } 45 | 46 | comptime { 47 | std.testing.refAllDecls(@This()); 48 | } 49 | }; 50 | 51 | comptime { 52 | std.testing.refAllDecls(@This()); 53 | } 54 | -------------------------------------------------------------------------------- /src/hashsig.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Similarity signature of arbitrary text content based on line hashes 9 | pub const Hashsig = opaque { 10 | pub fn deinit(self: *Hashsig) void { 11 | if (internal.trace_log) log.debug("Hashsig.deinit called", .{}); 12 | 13 | c.git_hashsig_free(@as(*c.git_hashsig, @ptrCast(self))); 14 | } 15 | 16 | /// Measure similarity score between two similarity signatures 17 | /// 18 | /// Returns [0 to 100] as the similarity score 19 | pub fn compare(self: *const Hashsig, other: *const Hashsig) !u7 { 20 | if (internal.trace_log) log.debug("Hashsig.compare called", .{}); 21 | 22 | return @as( 23 | u7, 24 | @truncate(@as( 25 | c_uint, 26 | @intCast(try internal.wrapCallWithReturn("git_hashsig_compare", .{ 27 | @as(*const c.git_hashsig, @ptrCast(self)), 28 | @as(*const c.git_hashsig, @ptrCast(other)), 29 | })), 30 | )), 31 | ); 32 | } 33 | 34 | comptime { 35 | std.testing.refAllDecls(@This()); 36 | } 37 | }; 38 | 39 | /// Options for hashsig computation 40 | pub const HashsigOptions = struct { 41 | whitespace_mode: WhitespaceMode = .normal, 42 | 43 | /// Allow hashing of small files 44 | allow_small_files: bool = false, 45 | 46 | pub const WhitespaceMode = enum { 47 | /// Use all data 48 | normal, 49 | /// Ignore whitespace 50 | ignore_whitespace, 51 | /// Ignore \r and all space after \n 52 | smart_whitespace, 53 | }; 54 | 55 | comptime { 56 | std.testing.refAllDecls(@This()); 57 | } 58 | }; 59 | 60 | comptime { 61 | std.testing.refAllDecls(@This()); 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-libgit2 2 | 3 | ## Warning 4 | This project is not currently in a good state and is not 100% compatible with any of libgit2's releases. 5 | 6 | Work on this project stalled at step one in [TODO](TODO.md) 7 | 8 | --- 9 | 10 | Zig bindings to [libgit2](https://github.com/libgit2/libgit2) 11 | 12 | This is an in-progress zig binding to libgit2, unfortunately libgit2 doesn't full document all possible errors so every errorable function returns the full errorset. 13 | 14 | There is currently no plan to port all the headers within "include/git2/sys", if anyone requires any of that functionailty raise an issue. 15 | 16 | ## Files fully wrapped (others maybe partially complete) 17 | 18 | - [X] annotated_commit.h 19 | - [X] apply.h 20 | - [X] attr.h 21 | - [X] blame.h 22 | - [X] blob.h 23 | - [X] branch.h 24 | - [X] buffer.h 25 | - [X] cert.h 26 | - [X] checkout.h 27 | - [X] cherrypick.h 28 | - [X] clone.h 29 | - [X] commit.h 30 | - [X] common.h 31 | - [X] config.h 32 | - [X] credential.h 33 | - [X] describe.h 34 | - [ ] diff.h 35 | - [X] errors.h 36 | - [X] filter.h 37 | - [X] global.h 38 | - [X] graph.h 39 | - [X] ignore.h 40 | - [X] index.h 41 | - [X] indexer.h 42 | - [X] mailmap.h 43 | - [ ] merge.h 44 | - [X] message.h 45 | - [X] notes.h 46 | - [X] object.h 47 | - [ ] odb.h 48 | - [X] oid.h 49 | - [X] oidarray.h 50 | - [X] pack.h 51 | - [X] patch.h 52 | - [X] pathspec.h 53 | - [X] proxy.h 54 | - [X] rebase.h 55 | - [X] refdb.h 56 | - [X] reflog.h 57 | - [ ] refs.h 58 | - [X] refspec.h 59 | - [X] remote.h 60 | - [X] repository.h 61 | - [X] reset.h 62 | - [X] revert.h 63 | - [X] revparse.h 64 | - [X] revwalk.h 65 | - [X] signature.h 66 | - [X] stash.h 67 | - [X] status.h 68 | - [X] strarray.h 69 | - [ ] submodule.h 70 | - [X] tag.h 71 | - [X] trace.h 72 | - [X] transaction.h 73 | - [X] tree.h 74 | - [X] worktree.h 75 | - [X] sys/alloc.h 76 | - [X] sys/credential.h 77 | - [X] sys/diff.h 78 | - [X] sys/hashsig.h 79 | - [X] sys/path.h 80 | - [X] sys/repository.h 81 | -------------------------------------------------------------------------------- /src/strarray.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const StrArray = extern struct { 9 | strings: ?[*][*:0]u8 = null, 10 | count: usize = 0, 11 | 12 | pub fn fromSlice(slice: [][*:0]u8) StrArray { 13 | return .{ 14 | .strings = slice.ptr, 15 | .count = slice.len, 16 | }; 17 | } 18 | 19 | pub fn toSlice(self: StrArray) []const [*:0]const u8 { 20 | if (self.count == 0) return &[_][*:0]const u8{}; 21 | return @as([*]const [*:0]const u8, @ptrCast(self.strings))[0..self.count]; 22 | } 23 | 24 | /// This should be called only on `StrArray`'s provided by the library 25 | pub fn deinit(self: *StrArray) void { 26 | if (internal.trace_log) log.debug("StrArray.deinit called", .{}); 27 | 28 | if (@hasDecl(c, "git_strarray_dispose")) { 29 | c.git_strarray_dispose(@as(*c.git_strarray, @ptrCast(self))); 30 | } else { 31 | c.git_strarray_free(@as(*c.git_strarray, @ptrCast(self))); 32 | } 33 | } 34 | 35 | pub fn copy(self: StrArray) !StrArray { 36 | if (internal.trace_log) log.debug("StrArray.copy called", .{}); 37 | 38 | var result: StrArray = undefined; 39 | try internal.wrapCall("git_strarray_copy", .{ 40 | @as(*c.git_strarray, @ptrCast(&result)), 41 | @as(*const c.git_strarray, @ptrCast(&self)), 42 | }); 43 | 44 | return result; 45 | } 46 | 47 | test { 48 | try std.testing.expectEqual(@sizeOf(c.git_strarray), @sizeOf(StrArray)); 49 | try std.testing.expectEqual(@bitSizeOf(c.git_strarray), @bitSizeOf(StrArray)); 50 | } 51 | 52 | comptime { 53 | std.testing.refAllDecls(@This()); 54 | } 55 | }; 56 | 57 | comptime { 58 | std.testing.refAllDecls(@This()); 59 | } 60 | -------------------------------------------------------------------------------- /src/buffer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// A data buffer for exporting data from libgit2 9 | pub const Buf = extern struct { 10 | ptr: ?[*]u8 = null, 11 | asize: usize = 0, 12 | size: usize = 0, 13 | 14 | const zero_array = [_]u8{0}; 15 | const zero_slice = zero_array[0..0 :0]; 16 | 17 | pub fn toSlice(self: Buf) [:0]const u8 { 18 | return if (self.size == 0) 19 | zero_slice 20 | else 21 | self.ptr.?[0..self.size :0]; 22 | } 23 | 24 | /// Free the memory referred to by the `Buf` 25 | /// 26 | /// *Note*: This will not free the memory if it looks like it was not allocated by libgit2, but it will clear the buffer back 27 | /// to the empty state. 28 | pub fn deinit(self: *Buf) void { 29 | if (internal.trace_log) log.debug("Buf.deinit called", .{}); 30 | 31 | c.git_buf_dispose(@ptrCast(self)); 32 | } 33 | 34 | /// Resize the buffer allocation to make more space. 35 | /// 36 | /// If the buffer refers to memory that was not allocated by libgit2, then `ptr` will be replaced with a newly allocated block 37 | /// of data. Be careful so that memory allocated by the caller is not lost. 38 | /// If you pass `target_size` = 0 and the memory is not allocated by libgit2, this will allocate a new buffer of size `size` 39 | /// and copy the external data into it. 40 | /// 41 | /// Currently, this will never shrink a buffer, only expand it. 42 | /// 43 | /// If the allocation fails, this will return an error and the buffer will be marked as invalid for future operations, 44 | /// invaliding the contents. 45 | pub fn grow(self: *Buf, target_size: usize) !void { 46 | if (internal.trace_log) log.debug("Buf.grow called", .{}); 47 | 48 | try internal.wrapCall("git_buf_grow", .{ @as(*c.git_buf, @ptrCast(self)), target_size }); 49 | } 50 | 51 | pub fn isBinary(self: *const Buf) bool { 52 | if (internal.trace_log) log.debug("Buf.isBinary called", .{}); 53 | 54 | return c.git_buf_is_binary(@ptrCast(self)) == 1; 55 | } 56 | 57 | pub fn containsNull(self: Buf) bool { 58 | if (internal.trace_log) log.debug("Buf.containsNull called", .{}); 59 | 60 | return std.mem.indexOfScalar(u8, self.toSlice(), 0) != null; 61 | } 62 | 63 | test { 64 | try std.testing.expectEqual(@sizeOf(c.git_buf), @sizeOf(Buf)); 65 | try std.testing.expectEqual(@bitSizeOf(c.git_buf), @bitSizeOf(Buf)); 66 | } 67 | 68 | comptime { 69 | std.testing.refAllDecls(@This()); 70 | } 71 | }; 72 | 73 | comptime { 74 | std.testing.refAllDecls(@This()); 75 | } 76 | -------------------------------------------------------------------------------- /src/describe.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const DescribeOptions = struct { 9 | max_candidate_tags: c_uint = 10, 10 | describe_strategy: DescribeStrategy = .default, 11 | 12 | pattern: ?[:0]const u8, 13 | 14 | /// When calculating the distance from the matching tag or reference, only walk down the first-parent ancestry. 15 | only_follow_first_parent: bool = false, 16 | 17 | // If no matching tag or reference is found, the describe operation would normally fail. If this option is set, it will 18 | // instead fall back to showing the full id of the commit. 19 | show_commit_oid_as_fallback: bool = false, 20 | 21 | /// Reference lookup strategy 22 | /// 23 | /// These behave like the --tags and --all options to git-describe, namely they say to look for any reference in either 24 | /// refs/tags/ or refs/ respectively. 25 | pub const DescribeStrategy = enum(c_uint) { 26 | default = 0, 27 | tags, 28 | all, 29 | }; 30 | 31 | comptime { 32 | std.testing.refAllDecls(@This()); 33 | } 34 | }; 35 | 36 | pub const DescribeFormatOptions = struct { 37 | /// Size of the abbreviated commit id to use. This value is the lower bound for the length of the abbreviated string. 38 | abbreviated_size: c_uint = 7, 39 | 40 | /// Set to use the long format even when a shorter name could be used. 41 | always_use_long_format: bool = false, 42 | 43 | /// If the workdir is dirty and this is set, this string will be appended to the description string. 44 | dirty_suffix: ?[:0]const u8 = null, 45 | 46 | comptime { 47 | std.testing.refAllDecls(@This()); 48 | } 49 | }; 50 | 51 | pub const DescribeResult = opaque { 52 | pub fn format(self: *const DescribeResult, options: DescribeFormatOptions) !git.Buf { 53 | if (internal.trace_log) log.debug("DescribeResult.format called", .{}); 54 | 55 | var buf: git.Buf = .{}; 56 | 57 | const c_options = internal.make_c_option.describeFormatOptions(options); 58 | 59 | try internal.wrapCall("git_describe_format", .{ 60 | @as(*c.git_buf, @ptrCast(&buf)), 61 | @as(*const c.git_describe_result, @ptrCast(self)), 62 | &c_options, 63 | }); 64 | 65 | return buf; 66 | } 67 | 68 | pub fn deinit(self: *DescribeResult) void { 69 | if (internal.trace_log) log.debug("DescribeResult.deinit called", .{}); 70 | 71 | c.git_describe_result_free(@as(*c.git_describe_result, @ptrCast(self))); 72 | } 73 | 74 | comptime { 75 | std.testing.refAllDecls(@This()); 76 | } 77 | }; 78 | 79 | comptime { 80 | std.testing.refAllDecls(@This()); 81 | } 82 | -------------------------------------------------------------------------------- /src/status_list.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const StatusList = opaque { 9 | pub fn deinit(self: *StatusList) void { 10 | if (internal.trace_log) log.debug("StatusList.deinit called", .{}); 11 | 12 | c.git_status_list_free(@as(*c.git_status_list, @ptrCast(self))); 13 | } 14 | 15 | pub fn entryCount(self: *StatusList) usize { 16 | if (internal.trace_log) log.debug("StatusList.entryCount called", .{}); 17 | 18 | return c.git_status_list_entrycount(@as(*c.git_status_list, @ptrCast(self))); 19 | } 20 | 21 | pub fn statusByIndex(self: *StatusList, index: usize) ?*const StatusEntry { 22 | if (internal.trace_log) log.debug("StatusList.statusByIndex called", .{}); 23 | 24 | return @as( 25 | ?*const StatusEntry, 26 | @ptrCast(c.git_status_byindex(@as(*c.git_status_list, @ptrCast(self)), index)), 27 | ); 28 | } 29 | 30 | /// Get performance data for diffs from a StatusList 31 | pub fn getPerfData(self: *const StatusList) !git.DiffPerfData { 32 | if (internal.trace_log) log.debug("StatusList.getPerfData called", .{}); 33 | 34 | var c_ret = c.git_diff_perfdata{ 35 | .version = c.GIT_DIFF_PERFDATA_VERSION, 36 | .stat_calls = 0, 37 | .oid_calculations = 0, 38 | }; 39 | 40 | try internal.wrapCall("git_status_list_get_perfdata", .{ 41 | &c_ret, 42 | @as(*const c.git_status_list, @ptrCast(self)), 43 | }); 44 | 45 | return git.DiffPerfData{ 46 | .stat_calls = c_ret.stat_calls, 47 | .oid_calculations = c_ret.oid_calculations, 48 | }; 49 | } 50 | 51 | /// A status entry, providing the differences between the file as it exists in HEAD and the index, and providing the 52 | /// differences between the index and the working directory. 53 | pub const StatusEntry = extern struct { 54 | /// The status for this file 55 | status: git.FileStatus, 56 | 57 | /// information about the differences between the file in HEAD and the file in the index. 58 | head_to_index: *git.DiffDelta, 59 | 60 | /// information about the differences between the file in the index and the file in the working directory. 61 | index_to_workdir: *git.DiffDelta, 62 | 63 | test { 64 | try std.testing.expectEqual(@sizeOf(c.git_status_entry), @sizeOf(StatusEntry)); 65 | try std.testing.expectEqual(@bitSizeOf(c.git_status_entry), @bitSizeOf(StatusEntry)); 66 | } 67 | 68 | comptime { 69 | std.testing.refAllDecls(@This()); 70 | } 71 | }; 72 | 73 | comptime { 74 | std.testing.refAllDecls(@This()); 75 | } 76 | }; 77 | 78 | comptime { 79 | std.testing.refAllDecls(@This()); 80 | } 81 | -------------------------------------------------------------------------------- /src/alloc.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// An instance for a custom memory allocator 9 | /// 10 | /// Setting the pointers of this structure allows the developer to implement custom memory allocators. 11 | /// The global memory allocator can be set by using `Handle.optionSetAllocator` function. 12 | /// Keep in mind that all fields need to be set to a proper function. 13 | pub const GitAllocator = extern struct { 14 | /// Allocate `n` bytes of memory 15 | malloc: *const fn (n: usize, file: [*:0]const u8, line: c_int) callconv(.C) ?*anyopaque, 16 | 17 | /// Allocate memory for an array of `nelem` elements, where each element has a size of `elsize`. 18 | /// Returned memory shall be initialized to all-zeroes 19 | calloc: *const fn (nelem: usize, elsize: usize, file: [*:0]const u8, line: c_int) callconv(.C) ?*anyopaque, 20 | 21 | /// Allocate memory for the string `str` and duplicate its contents. 22 | strdup: *const fn (str: [*:0]const u8, file: [*:0]const u8, line: c_int) callconv(.C) [*:0]const u8, 23 | 24 | /// Equivalent to the `gstrdup` function, but only duplicating at most `n + 1` bytes 25 | strndup: *const fn (str: [*:0]const u8, n: usize, file: [*:0]const u8, line: c_int) callconv(.C) [*:0]const u8, 26 | 27 | /// Equivalent to `gstrndup`, but will always duplicate exactly `n` bytes of `str`. Thus, out of bounds reads at `str` 28 | /// may happen. 29 | substrdup: *const fn (str: [*:0]const u8, n: usize, file: [*:0]const u8, line: c_int) callconv(.C) [*:0]const u8, 30 | 31 | /// This function shall deallocate the old object `ptr` and return a pointer to a new object that has the size specified by 32 | /// size`. In case `ptr` is `null`, a new array shall be allocated. 33 | realloc: *const fn (ptr: ?*anyopaque, size: usize, file: [*:0]const u8, line: c_int) callconv(.C) ?*anyopaque, 34 | 35 | /// This function shall be equivalent to `grealloc`, but allocating `neleme * elsize` bytes. 36 | reallocarray: *const fn (ptr: ?*anyopaque, nelem: usize, elsize: usize, file: [*:0]const u8, line: c_int) callconv(.C) ?*anyopaque, 37 | 38 | /// This function shall allocate a new array of `nelem` elements, where each element has a size of `elsize` bytes. 39 | mallocarray: *const fn (nelem: usize, elsize: usize, file: [*:0]const u8, line: c_int) callconv(.C) ?*anyopaque, 40 | 41 | /// This function shall free the memory pointed to by `ptr`. In case `ptr` is `null`, this shall be a no-op. 42 | free: *const fn (ptr: ?*anyopaque) callconv(.C) void, 43 | 44 | test { 45 | try std.testing.expectEqual(@sizeOf(c.git_allocator), @sizeOf(GitAllocator)); 46 | try std.testing.expectEqual(@bitSizeOf(c.git_allocator), @bitSizeOf(GitAllocator)); 47 | } 48 | 49 | comptime { 50 | std.testing.refAllDecls(@This()); 51 | } 52 | }; 53 | 54 | comptime { 55 | std.testing.refAllDecls(@This()); 56 | } 57 | -------------------------------------------------------------------------------- /src/notes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Note = opaque { 9 | pub fn deinit(self: *Note) void { 10 | if (internal.trace_log) log.debug("Note.deinit called", .{}); 11 | 12 | c.git_note_free(@as(*c.git_note, @ptrCast(self))); 13 | } 14 | 15 | /// Get the note author 16 | pub fn author(self: *const Note) *const git.Signature { 17 | if (internal.trace_log) log.debug("Note.author called", .{}); 18 | 19 | return @as( 20 | *const git.Signature, 21 | @ptrCast(c.git_note_author(@as(*const c.git_note, @ptrCast(self)))), 22 | ); 23 | } 24 | 25 | /// Get the note committer 26 | pub fn committer(self: *const Note) *const git.Signature { 27 | if (internal.trace_log) log.debug("Note.committer called", .{}); 28 | 29 | return @as( 30 | *const git.Signature, 31 | @ptrCast(c.git_note_committer(@as(*const c.git_note, @ptrCast(self)))), 32 | ); 33 | } 34 | 35 | /// Get the note message 36 | pub fn message(self: *const Note) [:0]const u8 { 37 | if (internal.trace_log) log.debug("Note.message called", .{}); 38 | 39 | return std.mem.sliceTo( 40 | c.git_note_message(@as(*const c.git_note, @ptrCast(self))), 41 | 0, 42 | ); 43 | } 44 | 45 | /// Get the note id 46 | pub fn id(self: *const Note) *const git.Oid { 47 | if (internal.trace_log) log.debug("Note.id called", .{}); 48 | 49 | return @as( 50 | *const git.Oid, 51 | @ptrCast(c.git_note_id(@as(*const c.git_note, @ptrCast(self)))), 52 | ); 53 | } 54 | 55 | comptime { 56 | std.testing.refAllDecls(@This()); 57 | } 58 | }; 59 | 60 | pub const NoteIterator = opaque { 61 | /// Return the current item and advance the iterator internally to the next value 62 | pub fn next(self: *NoteIterator) !?NextItem { 63 | if (internal.trace_log) log.debug("NoteIterator.next called", .{}); 64 | 65 | var ret: NextItem = undefined; 66 | 67 | internal.wrapCall("git_note_next", .{ 68 | @as(*c.git_oid, @ptrCast(&ret.note_id)), 69 | @as(*c.git_oid, @ptrCast(&ret.annotated_id)), 70 | @as(*c.git_note_iterator, @ptrCast(self)), 71 | }) catch |err| switch (err) { 72 | git.GitError.IterOver => return null, 73 | else => |e| return e, 74 | }; 75 | 76 | return ret; 77 | } 78 | 79 | pub fn deinit(self: *NoteIterator) void { 80 | if (internal.trace_log) log.debug("NoteIterator.deinit called", .{}); 81 | 82 | c.git_note_iterator_free(@as(*c.git_note_iterator, @ptrCast(self))); 83 | } 84 | 85 | pub const NextItem = struct { 86 | note_id: git.Oid, 87 | annotated_id: git.Oid, 88 | }; 89 | 90 | comptime { 91 | std.testing.refAllDecls(@This()); 92 | } 93 | }; 94 | 95 | comptime { 96 | std.testing.refAllDecls(@This()); 97 | } 98 | -------------------------------------------------------------------------------- /src/proxy.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const git = @import("git.zig"); 4 | 5 | /// Options for connecting through a proxy. 6 | /// Note that not all types may be supported, depending on the platform and compilation options. 7 | pub const ProxyOptions = struct { 8 | /// The type of proxy to use, by URL, auto-detect. 9 | proxy_type: ProxyType = .none, 10 | 11 | /// The URL of the proxy. 12 | url: ?[:0]const u8 = null, 13 | 14 | /// This will be called if the remote host requires authentication in order to connect to it. 15 | /// 16 | /// Return 0 for success, < 0 to indicate an error, > 0 to indicate no credential was acquired 17 | /// Returning `errorToCInt(GitError.Passthrough)` will make libgit2 behave as though this field isn't set. 18 | /// 19 | /// ## Parameters 20 | /// * `out` - The newly created credential object. 21 | /// * `url` - The resource for which we are demanding a credential. 22 | /// * `username_from_url` - The username that was embedded in a "user\@host" remote url, or `null` if not included. 23 | /// * `allowed_types` - A bitmask stating which credential types are OK to return. 24 | /// * `payload` - The payload provided when specifying this callback. 25 | credentials: ?*const fn ( 26 | out: **git.Credential, 27 | url: [*:0]const u8, 28 | username_from_url: [*:0]const u8, 29 | allowed_types: git.CredentialType, 30 | payload: ?*anyopaque, 31 | ) callconv(.C) c_int = null, 32 | 33 | /// If cert verification fails, this will be called to let the user make the final decision of whether to allow the 34 | /// connection to proceed. Returns 0 to allow the connection or a negative value to indicate an error. 35 | /// 36 | /// Return 0 to proceed with the connection, < 0 to fail the connection or > 0 to indicate that the callback refused 37 | /// to act and that the existing validity determination should be honored 38 | /// 39 | /// ## Parameters 40 | /// * `cert` - The host certificate 41 | /// * `valid` - Whether the libgit2 checks (OpenSSL or WinHTTP) think this certificate is valid. 42 | /// * `host` - Hostname of the host libgit2 connected to 43 | /// * `payload` - Payload provided by the caller 44 | certificate_check: ?*const fn ( 45 | cert: *git.Certificate, 46 | valid: bool, 47 | host: [*:0]const u8, 48 | payload: ?*anyopaque, 49 | ) callconv(.C) c_int = null, 50 | 51 | /// Payload to be provided to the credentials and certificate check callbacks. 52 | payload: ?*anyopaque = null, 53 | 54 | pub const ProxyType = enum(c_uint) { 55 | /// Do not attempt to connect through a proxy 56 | /// 57 | /// If built against libcurl, it itself may attempt to connect 58 | /// to a proxy if the environment variables specify it. 59 | none = 0, 60 | 61 | /// Try to auto-detect the proxy from the git configuration. 62 | auto, 63 | 64 | /// Connect via the URL given in the options 65 | specified, 66 | }; 67 | 68 | comptime { 69 | std.testing.refAllDecls(@This()); 70 | } 71 | }; 72 | 73 | comptime { 74 | std.testing.refAllDecls(@This()); 75 | } 76 | -------------------------------------------------------------------------------- /src/signature.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// An action signature (e.g. for committers, taggers, etc) 9 | pub const Signature = extern struct { 10 | /// Use `name` 11 | z_name: [*:0]const u8, 12 | /// Use `email` 13 | z_email: [*:0]const u8, 14 | /// Time when the action happened 15 | when: Time, 16 | 17 | /// Full name of the author 18 | pub fn name(self: Signature) [:0]const u8 { 19 | return std.mem.sliceTo(self.z_name, 0); 20 | } 21 | 22 | /// Email of the author 23 | pub fn email(self: Signature) [:0]const u8 { 24 | return std.mem.sliceTo(self.z_email, 0); 25 | } 26 | 27 | /// Free an existing signature. 28 | pub fn deinit(self: *Signature) void { 29 | if (internal.trace_log) log.debug("Signature.deinit called", .{}); 30 | 31 | c.git_signature_free(@as(*c.git_signature, @ptrCast(self))); 32 | } 33 | 34 | /// Create a new signature by parsing the given buffer, which is expected to be in the format 35 | /// "Real Name timestamp tzoffset", where `timestamp` is the number of seconds since the Unix epoch and `tzoffset` is 36 | /// the timezone offset in `hhmm` format (note the lack of a colon separator). 37 | /// 38 | /// ## Parameters 39 | /// * `buf` - Signature string 40 | pub fn fromSlice(buf: [:0]const u8) !*Signature { 41 | if (internal.trace_log) log.debug("Signature.fromSlice called", .{}); 42 | 43 | var ret: *git.Signature = undefined; 44 | 45 | try internal.wrapCall("git_signature_from_buffer", .{ 46 | @as(*?*c.git_signature, @ptrCast(&ret)), 47 | buf.ptr, 48 | }); 49 | 50 | return ret; 51 | } 52 | 53 | /// Create a copy of an existing signature. All internal strings are also duplicated. 54 | pub fn duplicate(self: *const git.Signature) !*git.Signature { 55 | if (internal.trace_log) log.debug("Signature.duplicate called", .{}); 56 | 57 | var ret: *git.Signature = undefined; 58 | 59 | try internal.wrapCall("git_signature_dup", .{ 60 | @as(*?*c.git_signature, @ptrCast(&ret)), 61 | @as(*const c.git_signature, @ptrCast(self)), 62 | }); 63 | 64 | return ret; 65 | } 66 | 67 | test { 68 | try std.testing.expectEqual(@sizeOf(c.git_signature), @sizeOf(Signature)); 69 | try std.testing.expectEqual(@bitSizeOf(c.git_signature), @bitSizeOf(Signature)); 70 | } 71 | 72 | comptime { 73 | std.testing.refAllDecls(@This()); 74 | } 75 | }; 76 | 77 | pub const Time = extern struct { 78 | /// time in seconds from epoch 79 | time: SystemTime, 80 | /// timezone offset, in minutes 81 | offset: c_int, 82 | /// indicator for questionable '-0000' offsets in signature 83 | sign: u8, 84 | 85 | pub const SystemTime = c.git_time_t; 86 | 87 | test { 88 | try std.testing.expectEqual(@sizeOf(c.git_time), @sizeOf(Time)); 89 | try std.testing.expectEqual(@bitSizeOf(c.git_time), @bitSizeOf(Time)); 90 | } 91 | 92 | comptime { 93 | std.testing.refAllDecls(@This()); 94 | } 95 | }; 96 | 97 | comptime { 98 | std.testing.refAllDecls(@This()); 99 | } 100 | -------------------------------------------------------------------------------- /src/indexer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Indexer = opaque { 9 | /// Add data to the indexer 10 | /// 11 | /// ## Parameters 12 | /// * `data` - The data to add 13 | /// * `stats` - Stat storage 14 | pub fn append(self: *Indexer, data: []const u8, stats: *IndexerProgress) !void { 15 | if (internal.trace_log) log.debug("Indexer.append called", .{}); 16 | 17 | try internal.wrapCall("git_indexer_append", .{ 18 | @as(*c.git_indexer, @ptrCast(self)), 19 | data.ptr, 20 | data.len, 21 | @as(*c.git_indexer_progress, @ptrCast(stats)), 22 | }); 23 | } 24 | 25 | /// Finalize the pack and index 26 | /// 27 | /// Resolve any pending deltas and write out the index file 28 | /// 29 | /// ## Parameters 30 | /// * `data` - The data to add 31 | /// * `stats` - Stat storage 32 | pub fn commit(self: *Indexer, stats: *IndexerProgress) !void { 33 | if (internal.trace_log) log.debug("Indexer.commit called", .{}); 34 | 35 | try internal.wrapCall("git_indexer_commit", .{ 36 | @as(*c.git_indexer, @ptrCast(self)), 37 | @as(*c.git_indexer_progress, @ptrCast(stats)), 38 | }); 39 | } 40 | 41 | /// Get the packfile's hash 42 | /// 43 | /// A packfile's name is derived from the sorted hashing of all object names. This is only correct after the index has been 44 | /// finalized. 45 | pub fn hash(self: *const Indexer) ?*const git.Oid { 46 | if (internal.trace_log) log.debug("Indexer.hash called", .{}); 47 | 48 | return @as( 49 | ?*const git.Oid, 50 | @ptrCast(c.git_indexer_hash(@as(*const c.git_indexer, @ptrCast(self)))), 51 | ); 52 | } 53 | 54 | pub fn deinit(self: *Indexer) void { 55 | if (internal.trace_log) log.debug("Indexer.deinit called", .{}); 56 | 57 | c.git_indexer_free(@as(*c.git_indexer, @ptrCast(self))); 58 | } 59 | 60 | comptime { 61 | std.testing.refAllDecls(@This()); 62 | } 63 | }; 64 | 65 | /// This structure is used to provide callers information about the progress of indexing a packfile, either directly or part 66 | /// of a fetch or clone that downloads a packfile. 67 | pub const IndexerProgress = extern struct { 68 | /// number of objects in the packfile being indexed 69 | total_objects: c_int, 70 | 71 | /// received objects that have been hashed 72 | indexed_objects: c_int, 73 | 74 | /// received_objects: objects which have been downloaded 75 | received_objects: c_int, 76 | 77 | /// locally-available objects that have been injected in order to fix a thin pack 78 | local_objects: c_int, 79 | 80 | /// number of deltas in the packfile being indexed 81 | total_deltas: c_int, 82 | 83 | /// received deltas that have been indexed 84 | indexed_deltas: c_int, 85 | 86 | /// size of the packfile received up to now 87 | received_bytes: usize, 88 | 89 | test { 90 | try std.testing.expectEqual(@sizeOf(c.git_indexer_progress), @sizeOf(IndexerProgress)); 91 | try std.testing.expectEqual(@bitSizeOf(c.git_indexer_progress), @bitSizeOf(IndexerProgress)); 92 | } 93 | 94 | comptime { 95 | std.testing.refAllDecls(@This()); 96 | } 97 | }; 98 | 99 | pub const IndexerOptions = struct { 100 | /// permissions to use creating packfile or 0 for defaults 101 | mode: c_uint = 0, 102 | 103 | /// do connectivity checks for the received pack 104 | verify: bool = false, 105 | }; 106 | 107 | comptime { 108 | std.testing.refAllDecls(@This()); 109 | } 110 | -------------------------------------------------------------------------------- /src/mailmap.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Mailmap = opaque { 9 | /// Free the mailmap and its associated memory. 10 | pub fn deinit(self: *Mailmap) void { 11 | if (internal.trace_log) log.debug("Mailmap.deinit called", .{}); 12 | 13 | c.git_mailmap_free(@as(*c.git_mailmap, @ptrCast(self))); 14 | } 15 | 16 | /// Add a single entry to the given mailmap object. If the entry already exists, it will be replaced with the new entry. 17 | /// 18 | /// ## Parameters 19 | /// * `real_name` - The real name to use, or `null` 20 | /// * `real_email` - The real email to use, or `null` 21 | /// * `replace_name` - The name to replace, or `null 22 | /// * `replace_email` - The email to replace 23 | pub fn addEntry( 24 | self: *Mailmap, 25 | real_name: ?[:0]const u8, 26 | real_email: ?[:0]const u8, 27 | replace_name: ?[:0]const u8, 28 | replace_email: [:0]const u8, 29 | ) !void { 30 | if (internal.trace_log) log.debug("Mailmap.addEntry called", .{}); 31 | 32 | const c_real_name = if (real_name) |ptr| ptr.ptr else null; 33 | const c_real_email = if (real_email) |ptr| ptr.ptr else null; 34 | const c_replace_name = if (replace_name) |ptr| ptr.ptr else null; 35 | 36 | try internal.wrapCall("git_mailmap_add_entry", .{ 37 | @as(*c.git_mailmap, @ptrCast(self)), 38 | c_real_name, 39 | c_real_email, 40 | c_replace_name, 41 | replace_email.ptr, 42 | }); 43 | } 44 | 45 | pub const ResolveResult = struct { 46 | real_name: [:0]const u8, 47 | real_email: [:0]const u8, 48 | }; 49 | 50 | /// Resolve a name and email to the corresponding real name and email. 51 | /// 52 | /// The lifetime of the strings are tied to `self`, `name`, and `email` parameters. 53 | /// 54 | /// ## Parameters 55 | /// * `self` - The mailmap to perform a lookup with (may be `null`) 56 | /// * `name` - The name to look up 57 | /// * `email` - The email to look up 58 | pub fn resolve(self: ?*const Mailmap, name: [:0]const u8, email: [:0]const u8) !ResolveResult { 59 | if (internal.trace_log) log.debug("Mailmap.resolve called", .{}); 60 | 61 | var real_name: ?[*:0]const u8 = undefined; 62 | var real_email: ?[*:0]const u8 = undefined; 63 | 64 | try internal.wrapCall("git_mailmap_resolve", .{ 65 | &real_name, 66 | &real_email, 67 | @as(*const c.git_mailmap, @ptrCast(self)), 68 | name.ptr, 69 | email.ptr, 70 | }); 71 | 72 | return ResolveResult{ 73 | .real_name = std.mem.sliceTo(real_name.?, 0), 74 | .real_email = std.mem.sliceTo(real_email.?, 0), 75 | }; 76 | } 77 | 78 | /// Resolve a signature to use real names and emails with a mailmap. 79 | /// 80 | /// Call `git.Signature.deinit` to free the data. 81 | /// 82 | /// ## Parameters 83 | /// * `signature` - Signature to resolve 84 | pub fn resolveSignature(self: *const Mailmap, signature: *const git.Signature) !*git.Signature { 85 | if (internal.trace_log) log.debug("Mailmap.resolveSignature called", .{}); 86 | 87 | var sig: *git.Signature = undefined; 88 | 89 | try internal.wrapCall("git_mailmap_resolve_signature", .{ 90 | @as(*?*c.git_signature, @ptrCast(&sig)), 91 | @as(*const c.git_mailmap, @ptrCast(self)), 92 | @as(*const c.git_signature, @ptrCast(signature)), 93 | }); 94 | 95 | return sig; 96 | } 97 | 98 | comptime { 99 | std.testing.refAllDecls(@This()); 100 | } 101 | }; 102 | 103 | comptime { 104 | std.testing.refAllDecls(@This()); 105 | } 106 | -------------------------------------------------------------------------------- /src/reference.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Reference = opaque { 9 | pub fn deinit(self: *Reference) void { 10 | if (internal.trace_log) log.debug("Reference.deinit called", .{}); 11 | 12 | c.git_reference_free(@as(*c.git_reference, @ptrCast(self))); 13 | } 14 | 15 | /// Delete an existing branch reference. 16 | /// 17 | /// Note that if the deletion succeeds, the reference will not be valid anymore, and should be freed immediately by the user 18 | /// using `deinit`. 19 | pub fn deleteBranch(self: *Reference) !void { 20 | if (internal.trace_log) log.debug("Reference.deleteBranch called", .{}); 21 | 22 | try internal.wrapCall("git_branch_delete", .{@as(*c.git_reference, @ptrCast(self))}); 23 | } 24 | 25 | pub fn annotatedCommitCreate(self: *const Reference, repository: *git.Repository) !*git.AnnotatedCommit { 26 | if (internal.trace_log) log.debug("Reference.annotatedCommitCreate called", .{}); 27 | 28 | var result: *git.AnnotatedCommit = undefined; 29 | 30 | try internal.wrapCall("git_annotated_commit_from_ref", .{ 31 | @as(*?*c.git_annotated_commit, @ptrCast(&result)), 32 | @as(*c.git_repository, @ptrCast(repository)), 33 | @as(*const c.git_reference, @ptrCast(self)), 34 | }); 35 | 36 | return result; 37 | } 38 | 39 | /// Move/rename an existing local branch reference. 40 | /// 41 | /// The new branch name will be checked for validity. 42 | /// 43 | /// Note that if the move succeeds, the old reference will not be valid anymore, and should be freed immediately by the user 44 | /// using `deinit`. 45 | pub fn move(self: *Reference, new_branch_name: [:0]const u8, force: bool) !*Reference { 46 | if (internal.trace_log) log.debug("Reference.move called", .{}); 47 | 48 | var ref: *Reference = undefined; 49 | 50 | try internal.wrapCall("git_branch_move", .{ 51 | @as(*?*c.git_reference, @ptrCast(&ref)), 52 | @as(*c.git_reference, @ptrCast(self)), 53 | new_branch_name.ptr, 54 | @intFromBool(force), 55 | }); 56 | 57 | return ref; 58 | } 59 | 60 | pub fn nameGet(self: *Reference) ![:0]const u8 { 61 | if (internal.trace_log) log.debug("Reference.nameGet called", .{}); 62 | 63 | var name: ?[*:0]const u8 = undefined; 64 | 65 | try internal.wrapCall("git_branch_name", .{ &name, @as(*const c.git_reference, @ptrCast(self)) }); 66 | 67 | return std.mem.sliceTo(name.?, 0); 68 | } 69 | 70 | pub fn upstreamGet(self: *Reference) !*Reference { 71 | if (internal.trace_log) log.debug("Reference.upstreamGet called", .{}); 72 | 73 | var ref: *Reference = undefined; 74 | 75 | try internal.wrapCall("git_branch_upstream", .{ 76 | @as(*?*c.git_reference, @ptrCast(&ref)), 77 | @as(*const c.git_reference, @ptrCast(self)), 78 | }); 79 | 80 | return ref; 81 | } 82 | 83 | pub fn upstreamSet(self: *Reference, branch_name: [:0]const u8) !void { 84 | if (internal.trace_log) log.debug("Reference.upstreamSet called", .{}); 85 | 86 | try internal.wrapCall("git_branch_set_upstream", .{ @as(*c.git_reference, @ptrCast(self)), branch_name.ptr }); 87 | } 88 | 89 | pub fn isHead(self: *const Reference) !bool { 90 | if (internal.trace_log) log.debug("Reference.isHead", .{}); 91 | 92 | return (try internal.wrapCallWithReturn("git_branch_is_head", .{@as(*const c.git_reference, @ptrCast(self))})) == 1; 93 | } 94 | 95 | pub fn isCheckedOut(self: *const Reference) !bool { 96 | if (internal.trace_log) log.debug("Reference.isCheckedOut", .{}); 97 | 98 | return (try internal.wrapCallWithReturn("git_branch_is_checked_out", .{@as(*const c.git_reference, @ptrCast(self))})) == 1; 99 | } 100 | 101 | comptime { 102 | std.testing.refAllDecls(@This()); 103 | } 104 | }; 105 | 106 | comptime { 107 | std.testing.refAllDecls(@This()); 108 | } 109 | -------------------------------------------------------------------------------- /src/tag.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Parsed representation of a tag object. 9 | pub const Tag = opaque { 10 | pub fn deinit(self: *Tag) void { 11 | if (internal.trace_log) log.debug("Tag.deinit called", .{}); 12 | 13 | c.git_tag_free(@as(*c.git_tag, @ptrCast(self))); 14 | } 15 | 16 | /// Get the id of a tag. 17 | pub fn id(self: *const Tag) *const git.Oid { 18 | if (internal.trace_log) log.debug("Tag.id called", .{}); 19 | 20 | return @as(*const git.Oid, @ptrCast(c.git_tag_id( 21 | @as(*const c.git_tag, @ptrCast(self)), 22 | ))); 23 | } 24 | 25 | /// Get the repository that contains the tag. 26 | pub fn owner(self: *const Tag) *git.Repository { 27 | if (internal.trace_log) log.debug("Tag.owner called", .{}); 28 | 29 | return @as(*git.Repository, @ptrCast(c.git_tag_owner( 30 | @as(*const c.git_tag, @ptrCast(self)), 31 | ))); 32 | } 33 | 34 | /// Get the tagged object of a tag, 35 | /// This method performs a repository lookup for the given object and returns it. 36 | pub fn target(self: *const Tag) !*git.Object { 37 | if (internal.trace_log) log.debug("Repository.target called", .{}); 38 | 39 | var ret: *git.Object = undefined; 40 | 41 | try internal.wrapCall("git_tag_target", .{ 42 | @as(*?*c.git_object, @ptrCast(&ret)), 43 | @as(*const c.git_tag, @ptrCast(self)), 44 | }); 45 | 46 | return ret; 47 | } 48 | 49 | /// Get the OID of the tagged object of a tag. 50 | pub fn targetId(self: *const Tag) *const git.Oid { 51 | if (internal.trace_log) log.debug("Tag.targetId called", .{}); 52 | 53 | return @as(*const git.Oid, @ptrCast(c.git_tag_target_id( 54 | @as(*const c.git_tag, @ptrCast(self)), 55 | ))); 56 | } 57 | 58 | /// Get the type of a tag's tagged object, 59 | pub fn targetType(self: *const Tag) git.ObjectType { 60 | if (internal.trace_log) log.debug("Tag.targetType called", .{}); 61 | 62 | return @as(git.ObjectType, @enumFromInt(c.git_tag_target_type( 63 | @as(*const c.git_tag, @ptrCast(self)), 64 | ))); 65 | } 66 | 67 | /// Get the name of a tag. 68 | pub fn name(self: *const Tag) [:0]const u8 { 69 | if (internal.trace_log) log.debug("Tag.name called", .{}); 70 | 71 | return std.mem.sliceTo(c.git_tag_name( 72 | @as(*const c.git_tag, @ptrCast(self)), 73 | ), 0); 74 | } 75 | 76 | /// Get the tagger (author) of a tag. 77 | pub fn author(self: *const Tag) ?*const git.Signature { 78 | if (internal.trace_log) log.debug("Tag.author called", .{}); 79 | 80 | return @as(?*const git.Signature, @ptrCast(c.git_tag_tagger( 81 | @as(*const c.git_tag, @ptrCast(self)), 82 | ))); 83 | } 84 | 85 | /// Get the message of a tag. 86 | pub fn message(self: *const Tag) ?[:0]const u8 { 87 | if (internal.trace_log) log.debug("Tag.message called", .{}); 88 | 89 | return if (c.git_tag_message(@as(*const c.git_tag, @ptrCast(self)))) |s| std.mem.sliceTo(s, 0) else null; 90 | } 91 | 92 | /// Recursively peel a tag until a non tag git_object is found 93 | /// 94 | /// The retrieved `git.Object` object is owned by the repository and should be closed with the `Object.deinit` method. 95 | pub fn peel(self: *Tag) !*git.Object { 96 | if (internal.trace_log) log.debug("Tag.peel called", .{}); 97 | 98 | var ret: *git.Object = undefined; 99 | 100 | try internal.wrapCall("git_tag_peel", .{ 101 | @as(*?*c.git_object, @ptrCast(&ret)), 102 | @as(*const c.git_tag, @ptrCast(self)), 103 | }); 104 | 105 | return ret; 106 | } 107 | 108 | /// Create an in-memory copy of a tag. The copy must be explicitly deinit'd or it will leak. 109 | pub fn duplicate(self: *Tag) !*Tag { 110 | if (internal.trace_log) log.debug("Tag.duplicate called", .{}); 111 | 112 | var tag: *Tag = undefined; 113 | 114 | try internal.wrapCall("git_tag_dup", .{ 115 | @as(*?*c.git_tag, @ptrCast(&tag)), 116 | @as(*c.git_tag, @ptrCast(self)), 117 | }); 118 | 119 | return tag; 120 | } 121 | 122 | comptime { 123 | std.testing.refAllDecls(@This()); 124 | } 125 | }; 126 | 127 | comptime { 128 | std.testing.refAllDecls(@This()); 129 | } 130 | -------------------------------------------------------------------------------- /src/refspec.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// A refspec specifies the mapping between remote and local reference names when fetch or pushing. 9 | pub const Refspec = opaque { 10 | /// Free a refspec object which has been created by Refspec.parse. 11 | pub fn deinit(self: *Refspec) void { 12 | if (internal.trace_log) log.debug("Refspec.deinit called", .{}); 13 | 14 | c.git_refspec_free(@as(*c.git_refspec, @ptrCast(self))); 15 | } 16 | 17 | /// Get the source specifier. 18 | pub fn source(self: *const Refspec) [:0]const u8 { 19 | if (internal.trace_log) log.debug("Refspec.source called", .{}); 20 | 21 | return std.mem.sliceTo( 22 | c.git_refspec_src(@as( 23 | *const c.git_refspec, 24 | @ptrCast(self), 25 | )), 26 | 0, 27 | ); 28 | } 29 | 30 | /// Get the destination specifier. 31 | pub fn destination(self: *const Refspec) [:0]const u8 { 32 | if (internal.trace_log) log.debug("Refspec.destination called", .{}); 33 | 34 | return std.mem.sliceTo( 35 | c.git_refspec_dst(@as( 36 | *const c.git_refspec, 37 | @ptrCast(self), 38 | )), 39 | 0, 40 | ); 41 | } 42 | 43 | /// Get the refspec's string. 44 | pub fn string(self: *const Refspec) [:0]const u8 { 45 | if (internal.trace_log) log.debug("Refspec.string called", .{}); 46 | 47 | return std.mem.sliceTo( 48 | c.git_refspec_string(@as( 49 | *const c.git_refspec, 50 | @ptrCast(self), 51 | )), 52 | 0, 53 | ); 54 | } 55 | 56 | /// Get the force update setting. 57 | pub fn isForceUpdate(self: *const Refspec) bool { 58 | if (internal.trace_log) log.debug("Refspec.isForceUpdate called", .{}); 59 | 60 | return c.git_refspec_force(@as(*const c.git_refspec, @ptrCast(self))) != 0; 61 | } 62 | 63 | /// Get the refspec's direction. 64 | pub fn direction(self: *const Refspec) git.Direction { 65 | if (internal.trace_log) log.debug("Refspec.direction called", .{}); 66 | 67 | return @as( 68 | git.Direction, 69 | @enumFromInt(c.git_refspec_direction(@as( 70 | *const c.git_refspec, 71 | @ptrCast(self), 72 | ))), 73 | ); 74 | } 75 | 76 | /// Check if a refspec's source descriptor matches a reference 77 | pub fn srcMatches(self: *const Refspec, refname: [:0]const u8) bool { 78 | if (internal.trace_log) log.debug("Refspec.srcMatches called", .{}); 79 | 80 | return c.git_refspec_src_matches( 81 | @as(*const c.git_refspec, @ptrCast(self)), 82 | refname.ptr, 83 | ) != 0; 84 | } 85 | 86 | /// Check if a refspec's destination descriptor matches a reference 87 | pub fn destMatches(self: *const Refspec, refname: [:0]const u8) bool { 88 | if (internal.trace_log) log.debug("Refspec.destMatches called", .{}); 89 | 90 | return c.git_refspec_dst_matches( 91 | @as(*const c.git_refspec, @ptrCast(self)), 92 | refname.ptr, 93 | ) != 0; 94 | } 95 | 96 | /// Transform a reference to its target following the refspec's rules 97 | /// 98 | /// # Parameters 99 | /// * `name` - The name of the reference to transform. 100 | pub fn transform(self: *const Refspec, name: [:0]const u8) !git.Buf { 101 | if (internal.trace_log) log.debug("Refspec.transform called", .{}); 102 | 103 | var ret: git.Buf = .{}; 104 | 105 | try internal.wrapCall("git_refspec_transform", .{ 106 | @as(*c.git_buf, @ptrCast(&ret)), 107 | @as(*const c.git_refspec, @ptrCast(self)), 108 | name.ptr, 109 | }); 110 | 111 | return ret; 112 | } 113 | 114 | /// Transform a target reference to its source reference following the refspec's rules 115 | /// 116 | /// # Parameters 117 | /// * `name` - The name of the reference to transform. 118 | pub fn rtransform(self: *const Refspec, name: [:0]const u8) !git.Buf { 119 | if (internal.trace_log) log.debug("Refspec.rtransform called", .{}); 120 | 121 | var ret: git.Buf = .{}; 122 | 123 | try internal.wrapCall("git_refspec_rtransform", .{ 124 | @as(*c.git_buf, @ptrCast(&ret)), 125 | @as(*const c.git_refspec, @ptrCast(self)), 126 | name.ptr, 127 | }); 128 | 129 | return ret; 130 | } 131 | 132 | comptime { 133 | std.testing.refAllDecls(@This()); 134 | } 135 | }; 136 | 137 | comptime { 138 | std.testing.refAllDecls(@This()); 139 | } 140 | -------------------------------------------------------------------------------- /src/transaction.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Transaction = opaque { 9 | /// Free the resources allocated by this transaction 10 | /// 11 | /// If any references remain locked, they will be unlocked without any changes made to them. 12 | pub fn deinit(self: *Transaction) !void { 13 | if (internal.trace_log) log.debug("Transaction.deinit called", .{}); 14 | 15 | c.git_transaction_free(@as(*c.git_transaction, @ptrCast(self))); 16 | } 17 | 18 | /// Lock a reference 19 | /// 20 | /// Lock the specified reference. This is the first step to updating a reference 21 | /// 22 | /// ## Parameters 23 | /// * `refname` - The reference to lock 24 | pub fn lockReference(self: *Transaction, refname: [:0]const u8) !void { 25 | if (internal.trace_log) log.debug("Transaction.lockReference called", .{}); 26 | 27 | try internal.wrapCall("git_transaction_lock_ref", .{ 28 | @as(*c.git_transaction, @ptrCast(self)), 29 | refname.ptr, 30 | }); 31 | } 32 | 33 | /// Set the target of a reference 34 | /// 35 | /// Set the target of the specified reference. This reference must be locked. 36 | /// 37 | /// ## Parameters 38 | /// * `refname` - The reference to lock 39 | /// * `target` - Target to set the reference to 40 | /// * `signature` - Signature to use in the reflog; pass `null` to read the identity from the config 41 | /// * `message` - Message to use in the reflog 42 | pub fn setTarget( 43 | self: *Transaction, 44 | refname: [:0]const u8, 45 | target: *const git.Oid, 46 | signature: ?*const git.Signature, 47 | message: [:0]const u8, 48 | ) !void { 49 | if (internal.trace_log) log.debug("Transaction.setTarget called", .{}); 50 | 51 | try internal.wrapCall("git_transaction_set_target", .{ 52 | @as(*c.git_transaction, @ptrCast(self)), 53 | refname.ptr, 54 | @as(*const c.git_oid, @ptrCast(target)), 55 | @as(?*const c.git_signature, @ptrCast(signature)), 56 | message.ptr, 57 | }); 58 | } 59 | 60 | /// Set the target of a reference 61 | /// 62 | /// Set the target of the specified reference. This reference must be locked. 63 | /// 64 | /// ## Parameters 65 | /// * `refname` - The reference to lock 66 | /// * `target` - Target to set the reference to 67 | /// * `signature` - Signature to use in the reflog; pass `null` to read the identity from the config 68 | /// * `message` - Message to use in the reflog 69 | pub fn setSymbolicTarget( 70 | self: *Transaction, 71 | refname: [:0]const u8, 72 | target: [:0]const u8, 73 | signature: ?*const git.Signature, 74 | message: [:0]const u8, 75 | ) !void { 76 | if (internal.trace_log) log.debug("Transaction.setSymbolicTarget called", .{}); 77 | 78 | try internal.wrapCall("git_transaction_set_symbolic_target", .{ 79 | @as(*c.git_transaction, @ptrCast(self)), 80 | refname.ptr, 81 | target.ptr, 82 | @as(?*const c.git_signature, @ptrCast(signature)), 83 | message.ptr, 84 | }); 85 | } 86 | 87 | /// Set the reflog of a reference 88 | /// 89 | /// Set the specified reference's reflog. If this is combined with setting the target, that update won't be written to the 90 | /// reflog. 91 | /// 92 | /// ## Parameters 93 | /// * `refname` - The reference to lock 94 | /// * `reflog` - The reflog as it should be written out 95 | pub fn setReflog(self: *Transaction, refname: [:0]const u8, reflog: *const git.Reflog) !void { 96 | if (internal.trace_log) log.debug("Transaction.setReflog called", .{}); 97 | 98 | try internal.wrapCall("git_transaction_set_reflog", .{ 99 | @as(*c.git_transaction, @ptrCast(self)), 100 | refname.ptr, 101 | @as(?*const c.git_reflog, @ptrCast(reflog)), 102 | }); 103 | } 104 | 105 | /// Remove a reference 106 | /// 107 | /// ## Parameters 108 | /// * `refname` - The reference to remove 109 | pub fn remove(self: *Transaction, refname: [:0]const u8) !void { 110 | if (internal.trace_log) log.debug("Transaction.remove called", .{}); 111 | 112 | try internal.wrapCall("git_transaction_remove", .{ 113 | @as(*c.git_transaction, @ptrCast(self)), 114 | refname.ptr, 115 | }); 116 | } 117 | 118 | /// Commit the changes from the transaction 119 | /// 120 | /// Perform the changes that have been queued. The updates will be made one by one, and the first failure will stop the 121 | /// processing. 122 | pub fn commit(self: *Transaction) !void { 123 | if (internal.trace_log) log.debug("Transaction.commit called", .{}); 124 | 125 | try internal.wrapCall("git_transaction_commit", .{ 126 | @as(*c.git_transaction, @ptrCast(self)), 127 | }); 128 | } 129 | 130 | comptime { 131 | std.testing.refAllDecls(@This()); 132 | } 133 | }; 134 | 135 | comptime { 136 | std.testing.refAllDecls(@This()); 137 | } 138 | -------------------------------------------------------------------------------- /src/attribute.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Attribute = struct { 9 | z_attr: ?[*:0]const u8, 10 | 11 | /// Check if an attribute is set on. In core git parlance, this the value for "Set" attributes. 12 | /// 13 | /// For example, if the attribute file contains: 14 | /// *.c foo 15 | /// 16 | /// Then for file `xyz.c` looking up attribute "foo" gives a value for which this is true. 17 | pub fn isTrue(self: Attribute) bool { 18 | return self.getValue() == .true; 19 | } 20 | 21 | /// Checks if an attribute is set off. In core git parlance, this is the value for attributes that are "Unset" (not to be 22 | /// confused with values that a "Unspecified"). 23 | /// 24 | /// For example, if the attribute file contains: 25 | /// *.h -foo 26 | /// 27 | /// Then for file `zyx.h` looking up attribute "foo" gives a value for which this is true. 28 | pub fn isFalse(self: Attribute) bool { 29 | return self.getValue() == .false; 30 | } 31 | 32 | /// Checks if an attribute is unspecified. This may be due to the attribute not being mentioned at all or because the 33 | /// attribute was explicitly set unspecified via the `!` operator. 34 | /// 35 | /// For example, if the attribute file contains: 36 | /// *.c foo 37 | /// *.h -foo 38 | /// onefile.c !foo 39 | /// 40 | /// Then for `onefile.c` looking up attribute "foo" yields a value with of true. Also, looking up "foo" on file `onefile.rb` 41 | /// or looking up "bar" on any file will all give a value of true. 42 | pub fn isUnspecified(self: Attribute) bool { 43 | return self.getValue() == .unspecified; 44 | } 45 | 46 | /// Checks if an attribute is set to a value (as opposed to @"true", @"false" or unspecified). This would be the case if for a file 47 | /// with something like: 48 | /// *.txt eol=lf 49 | /// 50 | /// Given this, looking up "eol" for `onefile.txt` will give back the string "lf" and will return true. 51 | pub fn hasValue(self: Attribute) bool { 52 | return self.getValue() == .string; 53 | } 54 | 55 | pub fn getValue(self: Attribute) AttributeValue { 56 | return @enumFromInt(c.git_attr_value(self.z_attr)); 57 | } 58 | 59 | pub const AttributeValue = enum(c_uint) { 60 | /// The attribute has been left unspecified 61 | unspecified = 0, 62 | /// The attribute has been set 63 | true, 64 | /// The attribute has been unset 65 | false, 66 | /// This attribute has a value 67 | string, 68 | }; 69 | 70 | comptime { 71 | std.testing.refAllDecls(@This()); 72 | } 73 | }; 74 | 75 | pub const AttributeOptions = struct { 76 | flags: git.AttributeFlags, 77 | commit_id: *git.Oid, 78 | 79 | comptime { 80 | std.testing.refAllDecls(@This()); 81 | } 82 | }; 83 | 84 | pub const AttributeFlags = struct { 85 | location: Location = .file_then_index, 86 | 87 | /// Controls extended attribute behavior 88 | extended: Extended = .{}, 89 | 90 | pub const Location = enum(u32) { 91 | file_then_index = 0, 92 | index_then_file = 1, 93 | index_only = 2, 94 | }; 95 | 96 | pub const Extended = packed struct { 97 | z_padding1: u2 = 0, 98 | 99 | /// Normally, attribute checks include looking in the /etc (or system equivalent) directory for a `gitattributes` 100 | /// file. Passing this flag will cause attribute checks to ignore that file. Setting the `no_system` flag will cause 101 | /// attribute checks to ignore that file. 102 | no_system: bool = false, 103 | 104 | /// Passing the `include_head` flag will use attributes from a `.gitattributes` file in the repository 105 | /// at the HEAD revision. 106 | include_head: bool = false, 107 | 108 | /// Passing the `include_commit` flag will use attributes from a `.gitattributes` file in a specific 109 | /// commit. 110 | include_commit: if (HAS_INCLUDE_COMMIT) bool else void = if (HAS_INCLUDE_COMMIT) false else {}, 111 | 112 | z_padding2: if (HAS_INCLUDE_COMMIT) u27 else u28 = 0, 113 | 114 | const HAS_INCLUDE_COMMIT = @hasDecl(c, "GIT_ATTR_CHECK_INCLUDE_COMMIT"); 115 | 116 | pub fn format( 117 | value: Extended, 118 | comptime fmt: []const u8, 119 | options: std.fmt.FormatOptions, 120 | writer: anytype, 121 | ) !void { 122 | _ = fmt; 123 | return internal.formatWithoutFields( 124 | value, 125 | options, 126 | writer, 127 | &.{ "z_padding1", "z_padding2" }, 128 | ); 129 | } 130 | 131 | test { 132 | try std.testing.expectEqual(@sizeOf(u32), @sizeOf(Extended)); 133 | try std.testing.expectEqual(@bitSizeOf(u32), @bitSizeOf(Extended)); 134 | } 135 | 136 | comptime { 137 | std.testing.refAllDecls(@This()); 138 | } 139 | }; 140 | 141 | comptime { 142 | std.testing.refAllDecls(@This()); 143 | } 144 | }; 145 | 146 | comptime { 147 | std.testing.refAllDecls(@This()); 148 | } 149 | -------------------------------------------------------------------------------- /src/certificate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Certificate = extern struct { 9 | /// Type of certificate 10 | cert_type: CertificateType, 11 | 12 | pub const CertificateType = enum(c_uint) { 13 | /// No information about the certificate is available. This may happen when using curl. 14 | none = 0, 15 | /// The `data` argument to the callback will be a pointer to the DER-encoded data. 16 | x509, 17 | /// The `data` argument to the callback will be a pointer to a `HostkeyCertificate` structure. 18 | hostkey_libssh2, 19 | /// The `data` argument to the callback will be a pointer to a `git.StrArray` with `name:content` strings containing 20 | /// information about the certificate. This is used when using curl. 21 | strarray, 22 | }; 23 | 24 | /// Hostkey information taken from libssh2 25 | pub const HostkeyCertificate = extern struct { 26 | /// The parent cert 27 | parent: Certificate, 28 | 29 | available: HostkeyAvailableFields, 30 | 31 | /// Hostkey hash. If `available` has `md5` set, this will have the MD5 hash of the hostkey. 32 | hash_md5: [16]u8, 33 | 34 | /// Hostkey hash. If `available` has `sha1` set, this will have the SHA1 hash of the hostkey. 35 | hash_sha1: [20]u8, 36 | 37 | /// Hostkey hash. If `available` has `sha256` set, this will have the SHA256 hash of the hostkey. 38 | hash_sha256: [32]u8, 39 | 40 | /// Raw hostkey type. If `available` has `raw` set, this will have the type of the raw hostkey. 41 | raw_type: RawType, 42 | 43 | /// Pointer to the raw hostkey. If `available` has `raw` set, this will be the raw contents of the hostkey. 44 | hostkey: [*]const u8, 45 | 46 | /// Length of the raw hostkey. If `available` has `raw` set, this will have the length of the raw contents of the hostkey. 47 | hostkey_len: usize, 48 | 49 | /// A bitmask containing the available fields. 50 | pub const HostkeyAvailableFields = packed struct { 51 | /// MD5 is available 52 | md5: bool, 53 | /// SHA-1 is available 54 | sha1: bool, 55 | /// SHA-256 is available 56 | sha256: bool, 57 | /// Raw hostkey is available 58 | raw: bool, 59 | 60 | z_padding: u28 = 0, 61 | 62 | pub fn format( 63 | value: HostkeyAvailableFields, 64 | comptime fmt: []const u8, 65 | options: std.fmt.FormatOptions, 66 | writer: anytype, 67 | ) !void { 68 | _ = fmt; 69 | return internal.formatWithoutFields( 70 | value, 71 | options, 72 | writer, 73 | &.{"z_padding"}, 74 | ); 75 | } 76 | 77 | test { 78 | try std.testing.expectEqual(@sizeOf(c_uint), @sizeOf(HostkeyAvailableFields)); 79 | try std.testing.expectEqual(@bitSizeOf(c_uint), @bitSizeOf(HostkeyAvailableFields)); 80 | } 81 | 82 | comptime { 83 | std.testing.refAllDecls(@This()); 84 | } 85 | }; 86 | 87 | pub const RawType = enum(c_uint) { 88 | /// The raw key is of an unknown type. 89 | unknown = 0, 90 | /// The raw key is an RSA key. 91 | rsa = 1, 92 | /// The raw key is a DSS key. 93 | dss = 2, 94 | /// The raw key is a ECDSA 256 key. 95 | key_ecdsa_256 = 3, 96 | /// The raw key is a ECDSA 384 key. 97 | key_ecdsa_384 = 4, 98 | /// The raw key is a ECDSA 521 key. 99 | key_ecdsa_521 = 5, 100 | /// The raw key is a ED25519 key. 101 | key_ed25519 = 6, 102 | }; 103 | 104 | test { 105 | try std.testing.expectEqual(@sizeOf(c.git_cert_hostkey), @sizeOf(HostkeyCertificate)); 106 | try std.testing.expectEqual(@bitSizeOf(c.git_cert_hostkey), @bitSizeOf(HostkeyCertificate)); 107 | } 108 | 109 | comptime { 110 | std.testing.refAllDecls(@This()); 111 | } 112 | }; 113 | 114 | /// X.509 certificate information 115 | pub const x509Certificate = extern struct { 116 | parent: Certificate, 117 | 118 | /// Pointer to the X.509 certificate data 119 | data: *anyopaque, 120 | 121 | /// Length of the memory block pointed to by `data`. 122 | length: usize, 123 | 124 | test { 125 | try std.testing.expectEqual(@sizeOf(c.git_cert_x509), @sizeOf(x509Certificate)); 126 | try std.testing.expectEqual(@bitSizeOf(c.git_cert_x509), @bitSizeOf(x509Certificate)); 127 | } 128 | 129 | comptime { 130 | std.testing.refAllDecls(@This()); 131 | } 132 | }; 133 | 134 | test { 135 | try std.testing.expectEqual(@sizeOf(c.git_cert), @sizeOf(Certificate)); 136 | try std.testing.expectEqual(@bitSizeOf(c.git_cert), @bitSizeOf(Certificate)); 137 | } 138 | 139 | comptime { 140 | std.testing.refAllDecls(@This()); 141 | } 142 | }; 143 | 144 | comptime { 145 | std.testing.refAllDecls(@This()); 146 | } 147 | -------------------------------------------------------------------------------- /src/errors.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const GitError = error{ 9 | /// Generic error 10 | GenericError, 11 | /// Requested object could not be found 12 | NotFound, 13 | /// Object exists preventing operation 14 | Exists, 15 | /// More than one object matches 16 | Ambiguous, 17 | /// Output buffer too short to hold data 18 | BufferTooShort, 19 | /// A special error that is never generated by libgit2 code. You can return it from a callback (e.g to stop an iteration) 20 | /// to know that it was generated by the callback and not by libgit2. 21 | User, 22 | /// Operation not allowed on bare repository 23 | BareRepo, 24 | /// HEAD refers to branch with no commits 25 | UnbornBranch, 26 | /// Merge in progress prevented operation 27 | Unmerged, 28 | /// Reference was not fast-forwardable 29 | NonFastForwardable, 30 | /// Name/ref spec was not in a valid format 31 | InvalidSpec, 32 | /// Checkout conflicts prevented operation 33 | Conflict, 34 | /// Lock file prevented operation 35 | Locked, 36 | /// Reference value does not match expected 37 | Modifed, 38 | /// Authentication error 39 | Auth, 40 | /// Server certificate is invalid 41 | Certificate, 42 | /// Patch/merge has already been applied 43 | Applied, 44 | /// The requested peel operation is not possible 45 | Peel, 46 | /// Unexpected EOF 47 | EndOfFile, 48 | /// Invalid operation or input 49 | Invalid, 50 | /// Uncommitted changes in index prevented operation 51 | Uncommited, 52 | /// The operation is not valid for a directory 53 | Directory, 54 | /// A merge conflict exists and cannot continue 55 | MergeConflict, 56 | /// A user-configured callback refused to act 57 | Passthrough, 58 | /// Signals end of iteration with iterator 59 | IterOver, 60 | /// Internal only 61 | Retry, 62 | /// Hashsum mismatch in object 63 | Mismatch, 64 | /// Unsaved changes in the index would be overwritten 65 | IndexDirty, 66 | /// Patch application failed 67 | ApplyFail, 68 | }; 69 | 70 | /// Transform a `GitError` into the matching `libgit2` error code 71 | pub fn errorToCInt(err: GitError) c_int { 72 | return switch (err) { 73 | git.GitError.GenericError => c.GIT_ERROR, 74 | git.GitError.NotFound => c.GIT_ENOTFOUND, 75 | git.GitError.Exists => c.GIT_EEXISTS, 76 | git.GitError.Ambiguous => c.GIT_EAMBIGUOUS, 77 | git.GitError.BufferTooShort => c.GIT_EBUFS, 78 | git.GitError.User => c.GIT_EUSER, 79 | git.GitError.BareRepo => c.GIT_EBAREREPO, 80 | git.GitError.UnbornBranch => c.GIT_EUNBORNBRANCH, 81 | git.GitError.Unmerged => c.GIT_EUNMERGED, 82 | git.GitError.NonFastForwardable => c.GIT_ENONFASTFORWARD, 83 | git.GitError.InvalidSpec => c.GIT_EINVALIDSPEC, 84 | git.GitError.Conflict => c.GIT_ECONFLICT, 85 | git.GitError.Locked => c.GIT_ELOCKED, 86 | git.GitError.Modifed => c.GIT_EMODIFIED, 87 | git.GitError.Auth => c.GIT_EAUTH, 88 | git.GitError.Certificate => c.GIT_ECERTIFICATE, 89 | git.GitError.Applied => c.GIT_EAPPLIED, 90 | git.GitError.Peel => c.GIT_EPEEL, 91 | git.GitError.EndOfFile => c.GIT_EEOF, 92 | git.GitError.Invalid => c.GIT_EINVALID, 93 | git.GitError.Uncommited => c.GIT_EUNCOMMITTED, 94 | git.GitError.Directory => c.GIT_EDIRECTORY, 95 | git.GitError.MergeConflict => c.GIT_EMERGECONFLICT, 96 | git.GitError.Passthrough => c.GIT_PASSTHROUGH, 97 | git.GitError.IterOver => c.GIT_ITEROVER, 98 | git.GitError.Retry => c.GIT_RETRY, 99 | git.GitError.Mismatch => c.GIT_EMISMATCH, 100 | git.GitError.IndexDirty => c.GIT_EINDEXDIRTY, 101 | git.GitError.ApplyFail => c.GIT_EAPPLYFAIL, 102 | }; 103 | } 104 | 105 | pub const DetailedError = extern struct { 106 | raw_message: [*:0]const u8, 107 | class: ErrorClass, 108 | 109 | pub const ErrorClass = enum(c_int) { 110 | none = 0, 111 | no_memory, 112 | os, 113 | invalid, 114 | reference, 115 | zlib, 116 | repository, 117 | config, 118 | regex, 119 | odb, 120 | index, 121 | object, 122 | net, 123 | tag, 124 | tree, 125 | indexer, 126 | ssl, 127 | submodule, 128 | thread, 129 | stash, 130 | checkout, 131 | fetchhead, 132 | merge, 133 | ssh, 134 | filter, 135 | revert, 136 | callback, 137 | cherrypick, 138 | describe, 139 | rebase, 140 | filesystem, 141 | patch, 142 | worktree, 143 | sha1, 144 | http, 145 | internal, 146 | }; 147 | 148 | pub fn message(self: DetailedError) [:0]const u8 { 149 | return std.mem.sliceTo(self.raw_message, 0); 150 | } 151 | 152 | test { 153 | try std.testing.expectEqual(@sizeOf(c.git_error), @sizeOf(DetailedError)); 154 | try std.testing.expectEqual(@bitSizeOf(c.git_error), @bitSizeOf(DetailedError)); 155 | } 156 | 157 | comptime { 158 | std.testing.refAllDecls(@This()); 159 | } 160 | }; 161 | 162 | comptime { 163 | std.testing.refAllDecls(@This()); 164 | } 165 | -------------------------------------------------------------------------------- /src/internal/internal.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("c.zig"); 3 | const git = @import("../git.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | // TODO: Document 7 | const log_errors = std.meta.globalOption("libgit2_log_errors", bool) orelse false; 8 | 9 | // TODO: Document 10 | pub const trace_log = std.meta.globalOption("libgit2_trace_log", bool) orelse false; 11 | 12 | pub const make_c_option = @import("make_c_option.zig"); 13 | 14 | pub const has_libssh2 = @hasDecl(c, "LIBSSH2_VERSION"); 15 | 16 | pub const has_credential = @hasDecl(c, "git_credential"); 17 | pub const RawCredentialType = if (has_credential) c.git_credential else c.git_cred; 18 | 19 | pub inline fn boolToCInt(value: bool) c_int { 20 | return if (value) 0 else 1; 21 | } 22 | 23 | pub inline fn wrapCall(comptime name: []const u8, args: anytype) git.GitError!void { 24 | const result = @call(.auto, @field(c, name), args); 25 | 26 | if (result >= 0) return; 27 | 28 | return unwrapError(name, result); 29 | } 30 | 31 | pub inline fn wrapCallWithReturn( 32 | comptime name: []const u8, 33 | args: anytype, 34 | ) git.GitError!@typeInfo(@TypeOf(@field(c, name))).Fn.return_type.? { 35 | const result = @call(.auto, @field(c, name), args); 36 | 37 | if (result >= 0) return result; 38 | 39 | return unwrapError(name, result); 40 | } 41 | 42 | fn unwrapError(name: []const u8, value: c.git_error_code) git.GitError { 43 | const err = switch (value) { 44 | c.GIT_ERROR => git.GitError.GenericError, 45 | c.GIT_ENOTFOUND => git.GitError.NotFound, 46 | c.GIT_EEXISTS => git.GitError.Exists, 47 | c.GIT_EAMBIGUOUS => git.GitError.Ambiguous, 48 | c.GIT_EBUFS => git.GitError.BufferTooShort, 49 | c.GIT_EUSER => git.GitError.User, 50 | c.GIT_EBAREREPO => git.GitError.BareRepo, 51 | c.GIT_EUNBORNBRANCH => git.GitError.UnbornBranch, 52 | c.GIT_EUNMERGED => git.GitError.Unmerged, 53 | c.GIT_ENONFASTFORWARD => git.GitError.NonFastForwardable, 54 | c.GIT_EINVALIDSPEC => git.GitError.InvalidSpec, 55 | c.GIT_ECONFLICT => git.GitError.Conflict, 56 | c.GIT_ELOCKED => git.GitError.Locked, 57 | c.GIT_EMODIFIED => git.GitError.Modifed, 58 | c.GIT_EAUTH => git.GitError.Auth, 59 | c.GIT_ECERTIFICATE => git.GitError.Certificate, 60 | c.GIT_EAPPLIED => git.GitError.Applied, 61 | c.GIT_EPEEL => git.GitError.Peel, 62 | c.GIT_EEOF => git.GitError.EndOfFile, 63 | c.GIT_EINVALID => git.GitError.Invalid, 64 | c.GIT_EUNCOMMITTED => git.GitError.Uncommited, 65 | c.GIT_EDIRECTORY => git.GitError.Directory, 66 | c.GIT_EMERGECONFLICT => git.GitError.MergeConflict, 67 | c.GIT_PASSTHROUGH => git.GitError.Passthrough, 68 | c.GIT_ITEROVER => git.GitError.IterOver, 69 | c.GIT_RETRY => git.GitError.Retry, 70 | c.GIT_EMISMATCH => git.GitError.Mismatch, 71 | c.GIT_EINDEXDIRTY => git.GitError.IndexDirty, 72 | c.GIT_EAPPLYFAIL => git.GitError.ApplyFail, 73 | else => { 74 | log.err("encountered unknown libgit2 error: {}", .{value}); 75 | unreachable; 76 | }, 77 | }; 78 | 79 | // We dont want to output log messages in tests, as the error might be expected 80 | // also dont incur the cost of calling `getDetailedLastError` if we are not going to use it 81 | if (!@import("builtin").is_test and log_errors and @intFromEnum(std.log.Level.err) <= @intFromEnum(std.log.level)) { 82 | if (git.Handle.getDetailedLastError(undefined)) |detailed| { 83 | log.err("{s} failed with error {s}/{s} - {s}", .{ 84 | name, 85 | @errorName(err), 86 | @tagName(detailed.class), 87 | detailed.message(), 88 | }); 89 | } else { 90 | log.err("{s} failed with error {s}", .{ 91 | name, 92 | @errorName(err), 93 | }); 94 | } 95 | } 96 | 97 | return err; 98 | } 99 | 100 | pub fn formatWithoutFields( 101 | value: anytype, 102 | options: std.fmt.FormatOptions, 103 | writer: anytype, 104 | comptime blacklist: []const []const u8, 105 | ) !void { 106 | // This ANY const is a workaround for: https://github.com/ziglang/zig/issues/7948 107 | const ANY = "any"; 108 | 109 | const T = @TypeOf(value); 110 | 111 | switch (@typeInfo(T)) { 112 | .Struct => |info| { 113 | try writer.writeAll(@typeName(T)); 114 | try writer.writeAll("{"); 115 | comptime var i = 0; 116 | outer: inline for (info.fields) |f| { 117 | inline for (blacklist) |blacklist_item| { 118 | if (comptime std.mem.indexOf(u8, f.name, blacklist_item) != null) continue :outer; 119 | } 120 | 121 | if (i == 0) { 122 | try writer.writeAll(" ."); 123 | } else { 124 | try writer.writeAll(", ."); 125 | } 126 | 127 | try writer.writeAll(f.name); 128 | try writer.writeAll(" = "); 129 | try std.fmt.formatType(@field(value, f.name), ANY, options, writer, std.fmt.default_max_depth - 1); 130 | 131 | i += 1; 132 | } 133 | try writer.writeAll(" }"); 134 | }, 135 | else => { 136 | @compileError("Unimplemented for: " ++ @typeName(T)); 137 | }, 138 | } 139 | } 140 | 141 | comptime { 142 | std.testing.refAllDecls(@This()); 143 | } 144 | -------------------------------------------------------------------------------- /src/reflog.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Representation of a reference log 9 | pub const Reflog = opaque { 10 | /// Free the reflog 11 | pub fn deinit(self: *Reflog) void { 12 | if (internal.trace_log) log.debug("Reflog.deinit called", .{}); 13 | 14 | c.git_reflog_free(@as(*c.git_reflog, @ptrCast(self))); 15 | } 16 | 17 | /// Write an existing in-memory reflog object back to disk using an atomic file lock. 18 | pub fn write(self: *Reflog) !void { 19 | if (internal.trace_log) log.debug("Reflog.write called", .{}); 20 | 21 | try internal.wrapCall("git_reflog_write", .{ 22 | @as(*c.git_reflog, @ptrCast(self)), 23 | }); 24 | } 25 | 26 | /// Add a new entry to the in-memory reflog. 27 | /// 28 | /// ## Parameters 29 | /// * `id` - The OID the reference is now pointing to 30 | /// * `signature` - The signature of the committer 31 | /// * `msg` - The reflog message, optional 32 | pub fn append( 33 | self: *Reflog, 34 | id: *const git.Oid, 35 | signature: *const git.Signature, 36 | msg: ?[:0]const u8, 37 | ) !void { 38 | if (internal.trace_log) log.debug("Reflog.append called", .{}); 39 | 40 | const c_msg = if (msg) |s| s.ptr else null; 41 | 42 | try internal.wrapCall("git_reflog_append", .{ 43 | @as(*c.git_reflog, @ptrCast(self)), 44 | @as(*const c.git_oid, @ptrCast(id)), 45 | @as(*const c.git_signature, @ptrCast(signature)), 46 | c_msg, 47 | }); 48 | } 49 | 50 | /// Get the number of log entries in a reflog 51 | pub fn entryCount(self: *Reflog) usize { 52 | if (internal.trace_log) log.debug("Reflog.entryCount called", .{}); 53 | 54 | return c.git_reflog_entrycount( 55 | @as(*c.git_reflog, @ptrCast(self)), 56 | ); 57 | } 58 | 59 | /// Lookup an entry by its index 60 | /// 61 | /// Requesting the reflog entry with an index of 0 (zero) will return the most recently created entry. 62 | /// 63 | /// ## Parameters 64 | /// * `index` - The position of the entry to lookup. Should be less than `Reflog.entryCount()` 65 | pub fn getEntry(self: *const Reflog, index: usize) ?*const ReflogEntry { 66 | if (internal.trace_log) log.debug("Reflog.getEntry called", .{}); 67 | 68 | return @as( 69 | ?*const ReflogEntry, 70 | @ptrCast(c.git_reflog_entry_byindex( 71 | @as(*const c.git_reflog, @ptrCast(self)), 72 | index, 73 | )), 74 | ); 75 | } 76 | 77 | /// Remove an entry from the reflog by its index 78 | /// 79 | /// To ensure there's no gap in the log history, set `rewrite_previous_entry` param value to `true`. 80 | /// When deleting entry `n`, member old_oid of entry `n-1` (if any) will be updated with the value of member new_oid of entry 81 | /// `n+1`. 82 | /// 83 | /// ## Parameters 84 | /// * `index` - The position of the entry to lookup. Should be less than `Reflog.entryCount()` 85 | /// * `rewrite_previous_entry` - `true` to rewrite the history; `false` otherwise 86 | pub fn removeEntry(self: *Reflog, index: usize, rewrite_previous_entry: bool) !void { 87 | if (internal.trace_log) log.debug("Reflog.removeEntry called", .{}); 88 | 89 | try internal.wrapCall("git_reflog_drop", .{ 90 | @as(*c.git_reflog, @ptrCast(self)), 91 | index, 92 | @intFromBool(rewrite_previous_entry), 93 | }); 94 | } 95 | 96 | /// Representation of a reference log entry 97 | pub const ReflogEntry = opaque { 98 | /// Get the old oid 99 | pub fn oldId(self: *const ReflogEntry) *const git.Oid { 100 | if (internal.trace_log) log.debug("ReflogEntry.oldId called", .{}); 101 | 102 | return @as(*const git.Oid, @ptrCast(c.git_reflog_entry_id_old( 103 | @as(*const c.git_reflog_entry, @ptrCast(self)), 104 | ))); 105 | } 106 | 107 | /// Get the new oid 108 | pub fn newId(self: *const ReflogEntry) *const git.Oid { 109 | if (internal.trace_log) log.debug("ReflogEntry.newId called", .{}); 110 | 111 | return @as(*const git.Oid, @ptrCast(c.git_reflog_entry_id_new( 112 | @as(*const c.git_reflog_entry, @ptrCast(self)), 113 | ))); 114 | } 115 | 116 | /// Get the committer of this entry 117 | pub fn commiter(self: *const ReflogEntry) *const git.Signature { 118 | if (internal.trace_log) log.debug("ReflogEntry.commiter called", .{}); 119 | 120 | return @as(*const git.Signature, @ptrCast(c.git_reflog_entry_committer( 121 | @as(*const c.git_reflog_entry, @ptrCast(self)), 122 | ))); 123 | } 124 | 125 | /// Get the log message 126 | pub fn message(self: *const ReflogEntry) ?[:0]const u8 { 127 | if (internal.trace_log) log.debug("ReflogEntry.message called", .{}); 128 | 129 | const opt_ret = @as(?[*:0]const u8, @ptrCast(c.git_reflog_entry_message( 130 | @as(*const c.git_reflog_entry, @ptrCast(self)), 131 | ))); 132 | 133 | return if (opt_ret) |ret| std.mem.sliceTo(ret, 0) else null; 134 | } 135 | 136 | comptime { 137 | std.testing.refAllDecls(@This()); 138 | } 139 | }; 140 | 141 | comptime { 142 | std.testing.refAllDecls(@This()); 143 | } 144 | }; 145 | 146 | comptime { 147 | std.testing.refAllDecls(@This()); 148 | } 149 | -------------------------------------------------------------------------------- /src/worktree.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Worktree = opaque { 9 | pub fn deinit(self: *Worktree) void { 10 | if (internal.trace_log) log.debug("Worktree.deinit called", .{}); 11 | 12 | c.git_worktree_free(@as(*c.git_worktree, @ptrCast(self))); 13 | } 14 | 15 | /// Check if worktree is valid 16 | /// 17 | /// A valid worktree requires both the git data structures inside the linked parent repository and the linked working 18 | /// copy to be present. 19 | pub fn valid(self: *const Worktree) !void { 20 | if (internal.trace_log) log.debug("Worktree.valid called", .{}); 21 | 22 | try internal.wrapCall("git_worktree_validate", .{ 23 | @as(*const c.git_worktree, @ptrCast(self)), 24 | }); 25 | } 26 | 27 | /// Lock worktree if not already locked 28 | /// 29 | /// Lock a worktree, optionally specifying a reason why the linked working tree is being locked. 30 | /// 31 | /// ## Parameters 32 | /// * `reason` - Reason why the working tree is being locked 33 | pub fn lock(self: *Worktree, reason: ?[:0]const u8) !void { 34 | if (internal.trace_log) log.debug("Worktree.lock called", .{}); 35 | 36 | const c_reason = if (reason) |s| s.ptr else null; 37 | 38 | try internal.wrapCall("git_worktree_lock", .{ 39 | @as(*c.git_worktree, @ptrCast(self)), 40 | c_reason, 41 | }); 42 | } 43 | 44 | /// Unlock a locked worktree 45 | /// 46 | /// Returns `true` if the worktree was no locked 47 | pub fn unlock(self: *Worktree) !bool { 48 | if (internal.trace_log) log.debug("Worktree.unlock called", .{}); 49 | 50 | return (try internal.wrapCallWithReturn("git_worktree_unlock", .{@as(*c.git_worktree, @ptrCast(self))})) != 0; 51 | } 52 | 53 | /// Check if worktree is locked 54 | // 55 | /// A worktree may be locked if the linked working tree is stored on a portable device which is not available. 56 | /// 57 | /// Returns `null` if the worktree is *not* locked, returns the reason for the lock if it is locked. 58 | pub fn is_locked(self: *const Worktree) !?git.Buf { 59 | if (internal.trace_log) log.debug("Worktree.is_locked called", .{}); 60 | 61 | var ret: git.Buf = .{}; 62 | 63 | const locked = (try internal.wrapCallWithReturn("git_worktree_is_locked", .{ 64 | @as(*c.git_buf, @ptrCast(&ret)), 65 | @as(*const c.git_worktree, @ptrCast(self)), 66 | })) != 0; 67 | 68 | return if (locked) ret else null; 69 | } 70 | 71 | /// Retrieve the name of the worktree 72 | /// 73 | /// The slice returned is valid for the lifetime of the `Worktree` 74 | pub fn name(self: *Worktree) ![:0]const u8 { 75 | if (internal.trace_log) log.debug("Worktree.name called", .{}); 76 | 77 | return std.mem.sliceTo(c.git_worktree_name(@as(*c.git_worktree, @ptrCast(self))), 0); 78 | } 79 | 80 | /// Retrieve the path of the worktree 81 | /// 82 | /// The slice returned is valid for the lifetime of the `Worktree` 83 | pub fn path(self: *Worktree) ![:0]const u8 { 84 | if (internal.trace_log) log.debug("Worktree.path called", .{}); 85 | 86 | return std.mem.sliceTo(c.git_worktree_path(@as(*c.git_worktree, @ptrCast(self))), 0); 87 | } 88 | 89 | pub fn repositoryOpen(self: *Worktree) !*git.Repository { 90 | if (internal.trace_log) log.debug("Worktree.repositoryOpen called", .{}); 91 | 92 | var repo: *git.Repository = undefined; 93 | 94 | try internal.wrapCall("git_repository_open_from_worktree", .{ 95 | @as(*?*c.git_repository, @ptrCast(&repo)), 96 | @as(*c.git_worktree, @ptrCast(self)), 97 | }); 98 | 99 | return repo; 100 | } 101 | 102 | /// Is the worktree prunable with the given options? 103 | /// 104 | /// A worktree is not prunable in the following scenarios: 105 | /// 106 | /// - the worktree is linking to a valid on-disk worktree. The `valid` member will cause this check to be ignored. 107 | /// - the worktree is locked. The `locked` flag will cause this check to be ignored. 108 | /// 109 | /// If the worktree is not valid and not locked or if the above flags have been passed in, this function will return a 110 | /// `true` 111 | pub fn isPruneable(self: *Worktree, options: PruneOptions) !bool { 112 | if (internal.trace_log) log.debug("Worktree.isPruneable called", .{}); 113 | 114 | var c_options = internal.make_c_option.pruneOptions(options); 115 | 116 | return (try internal.wrapCallWithReturn("git_worktree_is_prunable", .{ 117 | @as(*c.git_worktree, @ptrCast(self)), 118 | &c_options, 119 | })) != 0; 120 | } 121 | 122 | /// Prune working tree 123 | /// 124 | /// Prune the working tree, that is remove the git data structures on disk. The repository will only be pruned of 125 | /// `Worktree.isPruneable` succeeds. 126 | pub fn prune(self: *Worktree, options: PruneOptions) !void { 127 | if (internal.trace_log) log.debug("Worktree.prune called", .{}); 128 | 129 | var c_options = internal.make_c_option.pruneOptions(options); 130 | 131 | try internal.wrapCall("git_worktree_prune", .{ 132 | @as(*c.git_worktree, @ptrCast(self)), 133 | &c_options, 134 | }); 135 | } 136 | 137 | comptime { 138 | std.testing.refAllDecls(@This()); 139 | } 140 | }; 141 | 142 | pub const WorktreeAddOptions = struct { 143 | /// lock newly created worktree 144 | lock: bool = false, 145 | 146 | /// reference to use for the new worktree HEAD 147 | ref: ?*git.Reference = null, 148 | 149 | comptime { 150 | std.testing.refAllDecls(@This()); 151 | } 152 | }; 153 | 154 | pub const PruneOptions = packed struct { 155 | /// Prune working tree even if working tree is valid 156 | valid: bool = false, 157 | /// Prune working tree even if it is locked 158 | locked: bool = false, 159 | /// Prune checked out working tree 160 | working_tree: bool = false, 161 | 162 | z_padding: u29 = 0, 163 | 164 | pub fn format( 165 | value: PruneOptions, 166 | comptime fmt: []const u8, 167 | options: std.fmt.FormatOptions, 168 | writer: anytype, 169 | ) !void { 170 | _ = fmt; 171 | return internal.formatWithoutFields( 172 | value, 173 | options, 174 | writer, 175 | &.{"z_padding"}, 176 | ); 177 | } 178 | 179 | test { 180 | try std.testing.expectEqual(@sizeOf(c.git_worktree_prune_t), @sizeOf(PruneOptions)); 181 | try std.testing.expectEqual(@bitSizeOf(c.git_worktree_prune_t), @bitSizeOf(PruneOptions)); 182 | } 183 | 184 | comptime { 185 | std.testing.refAllDecls(@This()); 186 | } 187 | 188 | comptime { 189 | std.testing.refAllDecls(@This()); 190 | } 191 | }; 192 | 193 | comptime { 194 | std.testing.refAllDecls(@This()); 195 | } 196 | -------------------------------------------------------------------------------- /src/pathspec.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Pathspec = opaque { 9 | /// Free a pathspec 10 | pub fn deinit(self: *Pathspec) void { 11 | if (internal.trace_log) log.debug("Pathspec.deinit called", .{}); 12 | 13 | c.git_pathspec_free(@as(*c.git_pathspec, @ptrCast(self))); 14 | } 15 | 16 | /// Try to match a path against a pathspec 17 | /// 18 | /// Unlike most of the other pathspec matching functions, this will not fall back on the native case-sensitivity for your 19 | /// platform. You must explicitly pass options to control case sensitivity or this will fall back on being case sensitive. 20 | /// 21 | /// ## Parameters 22 | /// * `options` - Options to control match 23 | /// * `path` - The pathname to attempt to match 24 | pub fn matchesPath(self: *const Pathspec, options: PathspecMatchOptions, path: [:0]const u8) !bool { 25 | if (internal.trace_log) log.debug("Pathspec.matchesPath called", .{}); 26 | 27 | return (try internal.wrapCallWithReturn("git_pathspec_matches_path", .{ 28 | @as(*const c.git_pathspec, @ptrCast(self)), 29 | @as(c.git_pathspec_flag_t, @bitCast(options)), 30 | path.ptr, 31 | })) != 0; 32 | } 33 | 34 | comptime { 35 | std.testing.refAllDecls(@This()); 36 | } 37 | }; 38 | 39 | /// Options controlling how pathspec match should be executed 40 | pub const PathspecMatchOptions = packed struct { 41 | /// `ignore_case` forces match to ignore case; otherwise match will use native case sensitivity of platform filesystem 42 | ignore_case: bool = false, 43 | 44 | /// `use_case` forces case sensitive match; otherwise match will use native case sensitivity of platform filesystem 45 | use_case: bool = false, 46 | 47 | /// `no_glob` disables glob patterns and just uses simple string comparison for matching 48 | no_glob: bool = false, 49 | 50 | /// `no_match_error` means the match functions return error `GitError.NotFound` if no matches are found; otherwise no 51 | /// matches is still success (return 0) but `PathspecMatchList.entryCount` will indicate 0 matches. 52 | no_match_error: bool = false, 53 | 54 | /// `find_failures` means that the `PathspecMatchList` should track which patterns matched which files so that at the end of 55 | /// the match we can identify patterns that did not match any files. 56 | find_failures: bool = false, 57 | 58 | /// failures_only means that the `PathspecMatchList` does not need to keep the actual matching filenames. 59 | /// Use this to just test if there were any matches at all or in combination with `find_failures` to validate a pathspec. 60 | failures_only: bool = false, 61 | 62 | z_padding: u26 = 0, 63 | 64 | pub fn format( 65 | value: PathspecMatchOptions, 66 | comptime fmt: []const u8, 67 | options: std.fmt.FormatOptions, 68 | writer: anytype, 69 | ) !void { 70 | _ = fmt; 71 | return internal.formatWithoutFields( 72 | value, 73 | options, 74 | writer, 75 | &.{"z_padding"}, 76 | ); 77 | } 78 | 79 | test { 80 | try std.testing.expectEqual(@sizeOf(c.git_pathspec_flag_t), @sizeOf(PathspecMatchOptions)); 81 | try std.testing.expectEqual(@bitSizeOf(c.git_pathspec_flag_t), @bitSizeOf(PathspecMatchOptions)); 82 | } 83 | 84 | comptime { 85 | std.testing.refAllDecls(@This()); 86 | } 87 | }; 88 | 89 | /// list of filenames matching a pathspec 90 | pub const PathspecMatchList = opaque { 91 | /// Free a pathspec match list 92 | pub fn deinit(self: *PathspecMatchList) void { 93 | if (internal.trace_log) log.debug("PathspecMatchList.deinit called", .{}); 94 | 95 | c.git_pathspec_match_list_free(@as(*c.git_pathspec_match_list, @ptrCast(self))); 96 | } 97 | 98 | /// Get the number of items in a match list. 99 | pub fn entryCount(self: *const PathspecMatchList) usize { 100 | if (internal.trace_log) log.debug("PathspecMatchList.entryCount called", .{}); 101 | 102 | return c.git_pathspec_match_list_entrycount( 103 | @as(*const c.git_pathspec_match_list, @ptrCast(self)), 104 | ); 105 | } 106 | 107 | /// Get a matching filename by position. 108 | /// 109 | /// This routine cannot be used if the match list was generated by `Diff.pathspecMatch`. If so, it will always return `null`. 110 | /// 111 | /// ## Parameters 112 | /// * `index` - The index into the list 113 | pub fn getEntry(self: *const PathspecMatchList, index: usize) ?[:0]const u8 { 114 | if (internal.trace_log) log.debug("PathspecMatchList.getEntry called", .{}); 115 | 116 | const opt_c_ptr = c.git_pathspec_match_list_entry( 117 | @as(*const c.git_pathspec_match_list, @ptrCast(self)), 118 | index, 119 | ); 120 | 121 | return if (opt_c_ptr) |c_ptr| std.mem.sliceTo(c_ptr, 0) else null; 122 | } 123 | 124 | /// Get a matching diff delta by position. 125 | /// 126 | /// This routine can only be used if the match list was generated by `Diff.pathspecMatch`. 127 | /// Otherwise it will always return `null`. 128 | /// 129 | /// ## Parameters 130 | /// * `index` - The index into the list 131 | pub fn getDiffEntry(self: *const PathspecMatchList, index: usize) ?*const git.DiffDelta { 132 | if (internal.trace_log) log.debug("PathspecMatchList.getDiffEntry called", .{}); 133 | 134 | return @as( 135 | ?*const git.DiffDelta, 136 | @ptrCast(c.git_pathspec_match_list_diff_entry( 137 | @as(*const c.git_pathspec_match_list, @ptrCast(self)), 138 | index, 139 | )), 140 | ); 141 | } 142 | 143 | /// Get the number of pathspec items that did not match. 144 | /// 145 | /// This will be zero unless you passed `PathspecMatchOptions.find_failures` when generating the `PathspecMatchList`. 146 | pub fn failedEntryCount(self: *const PathspecMatchList) usize { 147 | if (internal.trace_log) log.debug("PathspecMatchList.failedEntryCount called", .{}); 148 | 149 | return c.git_pathspec_match_list_failed_entrycount( 150 | @as(*const c.git_pathspec_match_list, @ptrCast(self)), 151 | ); 152 | } 153 | 154 | /// Get an original pathspec string that had no matches. 155 | /// 156 | /// This will be return `null` for positions out of range. 157 | /// 158 | /// ## Parameters 159 | /// * `index` - The index into the failed items 160 | pub fn getFailedEntry(self: *const PathspecMatchList, index: usize) ?[:0]const u8 { 161 | if (internal.trace_log) log.debug("PathspecMatchList.getFailedEntry called", .{}); 162 | 163 | const opt_c_ptr = c.git_pathspec_match_list_failed_entry( 164 | @as(*const c.git_pathspec_match_list, @ptrCast(self)), 165 | index, 166 | ); 167 | 168 | return if (opt_c_ptr) |c_ptr| std.mem.sliceTo(c_ptr, 0) else null; 169 | } 170 | 171 | comptime { 172 | std.testing.refAllDecls(@This()); 173 | } 174 | }; 175 | 176 | comptime { 177 | std.testing.refAllDecls(@This()); 178 | } 179 | -------------------------------------------------------------------------------- /src/oid.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Unique identity of any object (commit, tree, blob, tag). 9 | pub const Oid = extern struct { 10 | id: [20]u8, 11 | 12 | /// Minimum length (in number of hex characters, i.e. packets of 4 bits) of an oid prefix 13 | pub const min_prefix_len = c.GIT_OID_MINPREFIXLEN; 14 | 15 | /// Size (in bytes) of a hex formatted oid 16 | pub const hex_buffer_size = c.GIT_OID_HEXSZ; 17 | 18 | pub const zero: Oid = .{ .id = [_]u8{0} ** 20 }; 19 | 20 | pub fn formatHexAlloc(self: Oid, allocator: *std.mem.Allocator) ![]const u8 { 21 | const buf = try allocator.alloc(u8, hex_buffer_size); 22 | errdefer allocator.free(buf); 23 | return try self.formatHex(buf); 24 | } 25 | 26 | /// `buf` must be atleast `hex_buffer_size` long. 27 | pub fn formatHex(self: *const Oid, buf: []u8) ![]const u8 { 28 | if (buf.len < hex_buffer_size) return error.BufferTooShort; 29 | 30 | try internal.wrapCall("git_oid_fmt", .{ buf.ptr, @as(*const c.git_oid, @ptrCast(self)) }); 31 | 32 | return buf[0..hex_buffer_size]; 33 | } 34 | 35 | pub fn formatHexAllocZ(self: *const Oid, allocator: *std.mem.Allocator) ![:0]const u8 { 36 | const buf = try allocator.allocSentinel(u8, hex_buffer_size, 0); 37 | errdefer allocator.free(buf); 38 | return (try self.formatHex(buf))[0.. :0]; 39 | } 40 | 41 | /// `buf` must be atleast `hex_buffer_size + 1` long. 42 | pub fn formatHexZ(self: *const Oid, buf: []u8) ![:0]const u8 { 43 | if (buf.len < (hex_buffer_size + 1)) return error.BufferTooShort; 44 | 45 | try internal.wrapCall("git_oid_fmt", .{ buf.ptr, @as(*const c.git_oid, @ptrCast(&self)) }); 46 | buf[hex_buffer_size] = 0; 47 | 48 | return buf[0..hex_buffer_size :0]; 49 | } 50 | 51 | /// Allows partial output 52 | pub fn formatHexCountAlloc(self: Oid, allocator: *std.mem.Allocator, length: usize) ![]const u8 { 53 | const buf = try allocator.alloc(u8, length); 54 | errdefer allocator.free(buf); 55 | return try self.formatHexCount(buf, length); 56 | } 57 | 58 | /// Allows partial output 59 | pub fn formatHexCount(self: *const Oid, buf: []u8, length: usize) ![]const u8 { 60 | if (buf.len < length) return error.BufferTooShort; 61 | 62 | try internal.wrapCall("git_oid_nfmt", .{ buf.ptr, length, @as(*const c.git_oid, @ptrCast(self)) }); 63 | 64 | return buf[0..length]; 65 | } 66 | 67 | /// Allows partial output 68 | pub fn formatHexCountAllocZ(self: Oid, allocator: *std.mem.Allocator, length: usize) ![:0]const u8 { 69 | const buf = try allocator.allocSentinel(u8, length, 0); 70 | errdefer allocator.free(buf); 71 | return (try self.formatHexCount(buf, length))[0.. :0]; 72 | } 73 | 74 | /// Allows partial output 75 | pub fn formatHexCountZ(self: *const Oid, buf: []u8, length: usize) ![:0]const u8 { 76 | if (buf.len < (length + 1)) return error.BufferTooShort; 77 | 78 | try internal.wrapCall("git_oid_nfmt", .{ buf.ptr, length, @as(*const c.git_oid, @ptrCast(self)) }); 79 | buf[length] = 0; 80 | 81 | return buf[0..length :0]; 82 | } 83 | 84 | pub fn formatHexPathAlloc(self: Oid, allocator: *std.mem.Allocator) ![]const u8 { 85 | const buf = try allocator.alloc(u8, hex_buffer_size + 1); 86 | errdefer allocator.free(buf); 87 | return try self.formatHexPath(buf); 88 | } 89 | 90 | /// `buf` must be atleast `hex_buffer_size + 1` long. 91 | pub fn formatHexPath(self: *const Oid, buf: []u8) ![]const u8 { 92 | if (buf.len < hex_buffer_size + 1) return error.BufferTooShort; 93 | 94 | try internal.wrapCall("git_oid_pathfmt", .{ buf.ptr, @as(*const c.git_oid, @ptrCast(self)) }); 95 | 96 | return buf[0..hex_buffer_size]; 97 | } 98 | 99 | pub fn formatHexPathAllocZ(self: Oid, allocator: *std.mem.Allocator) ![:0]const u8 { 100 | const buf = try allocator.allocSentinel(u8, hex_buffer_size + 1, 0); 101 | errdefer allocator.free(buf); 102 | return (try self.formatHexPath(buf))[0.. :0]; 103 | } 104 | 105 | /// `buf` must be atleast `hex_buffer_size + 2` long. 106 | pub fn formatHexPathZ(self: *const Oid, buf: []u8) ![:0]const u8 { 107 | if (buf.len < (hex_buffer_size + 2)) return error.BufferTooShort; 108 | 109 | try internal.wrapCall("git_oid_pathfmt", .{ buf.ptr, @as(*const c.git_oid, @ptrCast(self)) }); 110 | buf[hex_buffer_size] = 0; 111 | 112 | return buf[0..hex_buffer_size :0]; 113 | } 114 | 115 | /// <0 if a < b; 0 if a == b; >0 if a > b 116 | pub fn compare(a: *const Oid, b: *const Oid) c_int { 117 | return c.git_oid_cmp(@as(*const c.git_oid, @ptrCast(a)), @as(*const c.git_oid, @ptrCast(b))); 118 | } 119 | 120 | pub fn equal(self: *const Oid, other: *const Oid) bool { 121 | return c.git_oid_equal(@as(*const c.git_oid, @ptrCast(self)), @as(*const c.git_oid, @ptrCast(other))) == 1; 122 | } 123 | 124 | pub fn compareCount(self: *const Oid, other: *const Oid, count: usize) bool { 125 | return c.git_oid_ncmp(@as(*const c.git_oid, @ptrCast(self)), @as(*const c.git_oid, @ptrCast(other)), count) == 0; 126 | } 127 | 128 | pub fn equalStr(self: *const Oid, str: [:0]const u8) bool { 129 | return c.git_oid_streq(@as(*const c.git_oid, @ptrCast(self)), str.ptr) == 0; 130 | } 131 | 132 | /// <0 if a < str; 0 if a == str; >0 if a > str 133 | pub fn compareStr(a: *const Oid, str: [:0]const u8) c_int { 134 | return c.git_oid_strcmp(@as(*const c.git_oid, @ptrCast(a)), str.ptr); 135 | } 136 | 137 | pub fn allZeros(self: Oid) bool { 138 | for (self.id) |i| if (i != 0) return false; 139 | return true; 140 | } 141 | 142 | test { 143 | try std.testing.expectEqual(@sizeOf(c.git_oid), @sizeOf(Oid)); 144 | try std.testing.expectEqual(@bitSizeOf(c.git_oid), @bitSizeOf(Oid)); 145 | } 146 | 147 | comptime { 148 | std.testing.refAllDecls(@This()); 149 | } 150 | }; 151 | 152 | /// The OID shortener is used to process a list of OIDs in text form and return the shortest length that would uniquely identify 153 | /// all of them. 154 | /// 155 | /// E.g. look at the result of `git log --abbrev-commit`. 156 | pub const OidShortener = opaque { 157 | pub fn add(self: *OidShortener, str: []const u8) !c_uint { 158 | if (internal.trace_log) log.debug("OidShortener.add called", .{}); 159 | 160 | if (str.len < Oid.hex_buffer_size) return error.BufferTooShort; 161 | 162 | const ret = try internal.wrapCallWithReturn("git_oid_shorten_add", .{ 163 | @as(*c.git_oid_shorten, @ptrCast(self)), 164 | str.ptr, 165 | }); 166 | 167 | return @as(c_uint, @bitCast(ret)); 168 | } 169 | 170 | pub fn deinit(self: *OidShortener) void { 171 | if (internal.trace_log) log.debug("OidShortener.deinit called", .{}); 172 | 173 | c.git_oid_shorten_free(@as(*c.git_oid_shorten, @ptrCast(self))); 174 | } 175 | 176 | comptime { 177 | std.testing.refAllDecls(@This()); 178 | } 179 | }; 180 | 181 | comptime { 182 | std.testing.refAllDecls(@This()); 183 | } 184 | -------------------------------------------------------------------------------- /src/merge.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const SimilarityMetric = extern struct { 9 | file_signature: *const fn (out: *?*anyopaque, file: *const git.DiffFile, full_path: [*:0]const u8, payload: ?*anyopaque) callconv(.C) c_int, 10 | 11 | buffer_signature: *const fn ( 12 | out: *?*anyopaque, 13 | file: *const git.DiffFile, 14 | buf: [*:0]const u8, 15 | buf_len: usize, 16 | payload: ?*anyopaque, 17 | ) callconv(.C) c_int, 18 | 19 | free_signature: *const fn (sig: ?*anyopaque, payload: ?*anyopaque) callconv(.C) void, 20 | 21 | similarity: *const fn (score: *c_int, siga: ?*anyopaque, sigb: ?*anyopaque, payload: ?*anyopaque) callconv(.C) c_int, 22 | 23 | payload: ?*anyopaque, 24 | 25 | test { 26 | try std.testing.expectEqual(@sizeOf(c.git_diff_similarity_metric), @sizeOf(SimilarityMetric)); 27 | try std.testing.expectEqual(@bitSizeOf(c.git_diff_similarity_metric), @bitSizeOf(SimilarityMetric)); 28 | } 29 | 30 | comptime { 31 | std.testing.refAllDecls(@This()); 32 | } 33 | }; 34 | 35 | pub const FileFavor = enum(c_uint) { 36 | /// When a region of a file is changed in both branches, a conflict will be recorded in the index so that `git_checkout` can 37 | /// produce a merge file with conflict markers in the working directory. 38 | /// This is the default. 39 | normal = 0, 40 | 41 | /// When a region of a file is changed in both branches, the file created in the index will contain the "ours" side of any 42 | /// conflicting region. The index will not record a conflict. 43 | ours = 1, 44 | 45 | /// When a region of a file is changed in both branches, the file created in the index will contain the "theirs" side of any 46 | /// conflicting region. The index will not record a conflict. 47 | theirs = 2, 48 | 49 | /// When a region of a file is changed in both branches, the file created in the index will contain each unique line from each 50 | /// side, which has the result of combining both files. The index will not record a conflict. 51 | @"union" = 3, 52 | }; 53 | 54 | pub const MergeOptions = struct { 55 | flags: MergeFlags = .{}, 56 | 57 | /// Similarity to consider a file renamed (default 50). If `Flags.find_renames` is enabled, added files will be compared with 58 | /// deleted files to determine their similarity. Files that are more similar than the rename threshold (percentage-wise) will 59 | /// be treated as a rename. 60 | rename_threshold: c_uint = 0, 61 | 62 | /// Maximum similarity sources to examine for renames (default 200). If the number of rename candidates (add / delete pairs) 63 | /// is greater than this value, inexact rename detection is aborted. 64 | /// 65 | /// This setting overrides the `merge.renameLimit` configuration value. 66 | target_limit: c_uint = 0, 67 | 68 | /// Pluggable similarity metric; pass `null` to use internal metric 69 | metric: ?*SimilarityMetric = null, 70 | 71 | /// Maximum number of times to merge common ancestors to build a virtual merge base when faced with criss-cross merges. When 72 | /// this limit is reached, the next ancestor will simply be used instead of attempting to merge it. The default is unlimited. 73 | recursion_limit: c_uint = 0, 74 | 75 | /// Default merge driver to be used when both sides of a merge have changed. The default is the `text` driver. 76 | default_driver: ?[:0]const u8 = null, 77 | 78 | /// Flags for handling conflicting content, to be used with the standard (`text`) merge driver. 79 | file_favor: FileFavor = .normal, 80 | 81 | file_flags: FileMergeFlags = .{}, 82 | 83 | pub const MergeFlags = packed struct { 84 | /// Detect renames that occur between the common ancestor and the "ours" side or the common ancestor and the "theirs" 85 | /// side. This will enable the ability to merge between a modified and renamed file. 86 | find_renames: bool = false, 87 | 88 | /// If a conflict occurs, exit immediately instead of attempting to continue resolving conflicts. The merge operation will 89 | /// fail with `GitError.MergeConflict` and no index will be returned. 90 | fail_on_conflict: bool = false, 91 | 92 | /// Do not write the REUC extension on the generated index 93 | skip_reuc: bool = false, 94 | 95 | /// If the commits being merged have multiple merge bases, do not build a recursive merge base (by merging the multiple 96 | /// merge bases), instead simply use the first base. This flag provides a similar merge base to `git-merge-resolve`. 97 | no_recursive: bool = false, 98 | 99 | z_padding: u12 = 0, 100 | z_padding2: u16 = 0, 101 | 102 | pub fn format( 103 | value: MergeFlags, 104 | comptime fmt: []const u8, 105 | options: std.fmt.FormatOptions, 106 | writer: anytype, 107 | ) !void { 108 | _ = fmt; 109 | return internal.formatWithoutFields( 110 | value, 111 | options, 112 | writer, 113 | &.{ "z_padding", "z_padding2" }, 114 | ); 115 | } 116 | 117 | test { 118 | try std.testing.expectEqual(@sizeOf(c.git_merge_flag_t), @sizeOf(MergeFlags)); 119 | try std.testing.expectEqual(@bitSizeOf(c.git_merge_flag_t), @bitSizeOf(MergeFlags)); 120 | } 121 | 122 | comptime { 123 | std.testing.refAllDecls(@This()); 124 | } 125 | }; 126 | 127 | /// File merging flags 128 | pub const FileMergeFlags = packed struct { 129 | /// Create standard conflicted merge files 130 | style_merge: bool = false, 131 | 132 | /// Create diff3-style files 133 | style_diff3: bool = false, 134 | 135 | /// Condense non-alphanumeric regions for simplified diff file 136 | simplify_alnum: bool = false, 137 | 138 | /// Ignore all whitespace 139 | ignore_whitespace: bool = false, 140 | 141 | /// Ignore changes in amount of whitespace 142 | ignore_whitespace_change: bool = false, 143 | 144 | /// Ignore whitespace at end of line 145 | ignore_whitespace_eol: bool = false, 146 | 147 | /// Use the "patience diff" algorithm 148 | diff_patience: bool = false, 149 | 150 | /// Take extra time to find minimal diff 151 | diff_minimal: bool = false, 152 | 153 | z_padding: u8 = 0, 154 | z_padding2: u16 = 0, 155 | 156 | pub fn format( 157 | value: FileMergeFlags, 158 | comptime fmt: []const u8, 159 | options: std.fmt.FormatOptions, 160 | writer: anytype, 161 | ) !void { 162 | _ = fmt; 163 | return internal.formatWithoutFields( 164 | value, 165 | options, 166 | writer, 167 | &.{ "z_padding", "z_padding2" }, 168 | ); 169 | } 170 | 171 | test { 172 | try std.testing.expectEqual(@sizeOf(c.git_merge_file_flag_t), @sizeOf(FileMergeFlags)); 173 | try std.testing.expectEqual(@bitSizeOf(c.git_merge_file_flag_t), @bitSizeOf(FileMergeFlags)); 174 | } 175 | 176 | comptime { 177 | std.testing.refAllDecls(@This()); 178 | } 179 | }; 180 | 181 | comptime { 182 | std.testing.refAllDecls(@This()); 183 | } 184 | }; 185 | 186 | comptime { 187 | std.testing.refAllDecls(@This()); 188 | } 189 | -------------------------------------------------------------------------------- /src/object.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Object = opaque { 9 | /// Close an open object 10 | /// 11 | /// This method instructs the library to close an existing object; note that `git.Object`s are owned and cached by 12 | /// the repository so the object may or may not be freed after this library call, depending on how aggressive is the 13 | /// caching mechanism used by the repository. 14 | /// 15 | /// IMPORTANT: 16 | /// It *is* necessary to call this method when you stop using an object. Failure to do so will cause a memory leak. 17 | pub fn deinit(self: *Object) void { 18 | if (internal.trace_log) log.debug("Object.deinit called", .{}); 19 | 20 | c.git_object_free(@as(*c.git_object, @ptrCast(self))); 21 | } 22 | 23 | /// Get the id (SHA1) of a repository object 24 | pub fn id(self: *const Object) *const git.Oid { 25 | if (internal.trace_log) log.debug("Object.id called", .{}); 26 | 27 | return @as( 28 | *const git.Oid, 29 | @ptrCast(c.git_object_id(@as(*const c.git_object, @ptrCast(self)))), 30 | ); 31 | } 32 | 33 | /// Get a short abbreviated OID string for the object 34 | /// 35 | /// This starts at the "core.abbrev" length (default 7 characters) and iteratively extends to a longer string if that length 36 | /// is ambiguous. 37 | /// The result will be unambiguous (at least until new objects are added to the repository). 38 | pub fn shortId(self: *const Object) !git.Buf { 39 | if (internal.trace_log) log.debug("Object.shortId called", .{}); 40 | 41 | var buf: git.Buf = .{}; 42 | 43 | try internal.wrapCall("git_object_short_id", .{ 44 | @as(*c.git_buf, @ptrCast(&buf)), 45 | @as(*const c.git_object, @ptrCast(self)), 46 | }); 47 | 48 | return buf; 49 | } 50 | 51 | /// Get the object type of an object 52 | pub fn objectType(self: *const Object) ObjectType { 53 | if (internal.trace_log) log.debug("Object.objectType called", .{}); 54 | 55 | return @as( 56 | ObjectType, 57 | @enumFromInt(c.git_object_type(@as(*const c.git_object, @ptrCast(self)))), 58 | ); 59 | } 60 | 61 | /// Get the repository that owns this object 62 | pub fn objectOwner(self: *const Object) *const git.Repository { 63 | if (internal.trace_log) log.debug("Object.objectOwner called", .{}); 64 | 65 | return @as( 66 | *const git.Repository, 67 | @ptrCast(c.git_object_owner(@as(*const c.git_object, @ptrCast(self)))), 68 | ); 69 | } 70 | 71 | /// Describe a commit 72 | /// 73 | /// Perform the describe operation on the given committish object. 74 | pub fn describe(self: *Object, options: git.DescribeOptions) !*git.DescribeResult { 75 | if (internal.trace_log) log.debug("Object.describe called", .{}); 76 | 77 | var result: *git.DescribeResult = undefined; 78 | 79 | var c_options = internal.make_c_option.describeOptions(options); 80 | 81 | try internal.wrapCall("git_describe_commit", .{ 82 | @as(*?*c.git_describe_result, @ptrCast(&result)), 83 | @as(*c.git_object, @ptrCast(self)), 84 | &c_options, 85 | }); 86 | 87 | return result; 88 | } 89 | 90 | /// Lookup an object that represents a tree entry. 91 | /// 92 | /// ## Parameters 93 | /// * `self` - Root object that can be peeled to a tree 94 | /// * `path` - Relative path from the root object to the desired object 95 | /// * `object_type` - Type of object desired 96 | pub fn lookupByPath(self: *const Object, path: [:0]const u8, object_type: ObjectType) !*Object { 97 | if (internal.trace_log) log.debug("Object.lookupByPath called", .{}); 98 | 99 | var ret: *Object = undefined; 100 | 101 | try internal.wrapCall("git_object_lookup_bypath", .{ 102 | @as(*?*c.git_object, @ptrCast(&ret)), 103 | @as(*const c.git_object, @ptrCast(self)), 104 | path.ptr, 105 | @intFromEnum(object_type), 106 | }); 107 | 108 | return ret; 109 | } 110 | 111 | /// Recursively peel an object until an object of the specified type is met. 112 | /// 113 | /// If the query cannot be satisfied due to the object model, `error.InvalidSpec` will be returned (e.g. trying to peel a blob 114 | /// to a tree). 115 | /// 116 | /// If you pass `ObjectType.any` as the target type, then the object will be peeled until the type changes. 117 | /// A tag will be peeled until the referenced object is no longer a tag, and a commit will be peeled to a tree. 118 | /// Any other object type will return `error.InvalidSpec`. 119 | /// 120 | /// If peeling a tag we discover an object which cannot be peeled to the target type due to the object model, `error.Peel` 121 | /// will be returned. 122 | /// 123 | /// You must `deinit` the returned object. 124 | /// 125 | /// ## Parameters 126 | /// * `target_type` - The type of the requested object 127 | pub fn peel(self: *const Object, target_type: ObjectType) !*git.Object { 128 | if (internal.trace_log) log.debug("Object.peel called", .{}); 129 | 130 | var ret: *Object = undefined; 131 | 132 | try internal.wrapCall("git_object_peel", .{ 133 | @as(*?*c.git_object, @ptrCast(&ret)), 134 | @as(*const c.git_object, @ptrCast(self)), 135 | @intFromEnum(target_type), 136 | }); 137 | 138 | return ret; 139 | } 140 | 141 | /// Create an in-memory copy of a Git object. The copy must be explicitly `deinit`'d or it will leak. 142 | pub fn duplicate(self: *Object) *Object { 143 | if (internal.trace_log) log.debug("Object.duplicate called", .{}); 144 | 145 | var ret: *Object = undefined; 146 | 147 | _ = c.git_object_dup( 148 | @as(*?*c.git_object, @ptrCast(&ret)), 149 | @as(*c.git_object, @ptrCast(self)), 150 | ); 151 | 152 | return ret; 153 | } 154 | 155 | comptime { 156 | std.testing.refAllDecls(@This()); 157 | } 158 | }; 159 | 160 | /// Basic type (loose or packed) of any Git object. 161 | pub const ObjectType = enum(c_int) { 162 | /// Object can be any of the following 163 | any = -2, 164 | /// Object is invalid. 165 | invalid = -1, 166 | /// A commit object. 167 | commit = 1, 168 | /// A tree (directory listing) object. 169 | tree = 2, 170 | /// A file revision object. 171 | blob = 3, 172 | /// An annotated tag object. 173 | tag = 4, 174 | /// A delta, base is given by an offset. 175 | ofs_delta = 6, 176 | /// A delta, base is given by object id. 177 | ref_delta = 7, 178 | 179 | /// Convert an object type to its string representation. 180 | pub fn toString(self: ObjectType) [:0]const u8 { 181 | return std.mem.sliceTo( 182 | c.git_object_type2string(@intFromEnum(self)), 183 | 0, 184 | ); 185 | } 186 | 187 | /// Convert a string object type representation to it's `ObjectType`. 188 | /// 189 | /// If the given string is not a valid object type `.invalid` is returned. 190 | pub fn fromString(str: [:0]const u8) ObjectType { 191 | return @as(ObjectType, @enumFromInt(c.git_object_string2type(str.ptr))); 192 | } 193 | 194 | /// Determine if the given `ObjectType` is a valid loose object type. 195 | pub fn validLoose(self: ObjectType) bool { 196 | return c.git_object_typeisloose(@intFromEnum(self)) != 0; 197 | } 198 | }; 199 | 200 | comptime { 201 | std.testing.refAllDecls(@This()); 202 | } 203 | -------------------------------------------------------------------------------- /src/blame.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Blame = opaque { 9 | pub fn deinit(self: *Blame) void { 10 | if (internal.trace_log) log.debug("Blame.deinit called", .{}); 11 | 12 | c.git_blame_free(@ptrCast(self)); 13 | } 14 | 15 | pub fn hunkCount(self: *Blame) u32 { 16 | if (internal.trace_log) log.debug("Blame.hunkCount called", .{}); 17 | 18 | return c.git_blame_get_hunk_count(@ptrCast(self)); 19 | } 20 | 21 | pub fn hunkByIndex(self: *Blame, index: u32) ?*const BlameHunk { 22 | if (internal.trace_log) log.debug("Blame.hunkByIndex called", .{}); 23 | 24 | return @ptrCast(c.git_blame_get_hunk_byindex(@ptrCast(self), index)); 25 | } 26 | 27 | pub fn hunkByLine(self: *Blame, line: usize) ?*const BlameHunk { 28 | if (internal.trace_log) log.debug("Blame.hunkByLine called", .{}); 29 | 30 | return @ptrCast(c.git_blame_get_hunk_byline(@ptrCast(self), line)); 31 | } 32 | 33 | /// Get blame data for a file that has been modified in memory. The `reference` parameter is a pre-calculated blame for the 34 | /// in-odb history of the file. This means that once a file blame is completed (which can be expensive), updating the buffer 35 | /// blame is very fast. 36 | /// 37 | /// Lines that differ between the buffer and the committed version are marked as having a zero OID for their final_commit_id. 38 | pub fn blameBuffer(self: *Blame, buffer: [:0]const u8) !*git.Blame { 39 | if (internal.trace_log) log.debug("Blame.blameBuffer called", .{}); 40 | 41 | var blame: *git.Blame = undefined; 42 | 43 | try internal.wrapCall("git_blame_buffer", .{ 44 | @as(*?*c.git_blame, @ptrCast(&blame)), 45 | @as(*c.git_blame, @ptrCast(self)), 46 | buffer.ptr, 47 | buffer.len, 48 | }); 49 | 50 | return blame; 51 | } 52 | 53 | comptime { 54 | std.testing.refAllDecls(@This()); 55 | } 56 | }; 57 | 58 | pub const BlameHunk = extern struct { 59 | /// The number of lines in this hunk. 60 | lines_in_hunk: usize, 61 | 62 | /// The OID of the commit where this line was last changed. 63 | final_commit_id: git.Oid, 64 | 65 | /// The 1-based line number where this hunk begins, in the final version 66 | /// of the file. 67 | final_start_line_number: usize, 68 | 69 | /// The author of `final_commit_id`. If `use_mailmap` has been 70 | /// specified, it will contain the canonical real name and email address. 71 | final_signature: *git.Signature, 72 | 73 | /// The OID of the commit where this hunk was found. 74 | /// This will usually be the same as `final_commit_id`, except when 75 | /// `any_commit_copies` has been specified. 76 | orig_commit_id: git.Oid, 77 | 78 | /// The path to the file where this hunk originated, as of the commit 79 | /// specified by `orig_commit_id`. 80 | /// Use `origPath` 81 | z_orig_path: [*:0]const u8, 82 | 83 | /// The 1-based line number where this hunk begins in the file named by 84 | /// `orig_path` in the commit specified by `orig_commit_id`. 85 | orig_start_line_number: usize, 86 | 87 | /// The author of `orig_commit_id`. If `use_mailmap` has been 88 | /// specified, it will contain the canonical real name and email address. 89 | orig_signature: *git.Signature, 90 | 91 | /// The 1 iff the hunk has been tracked to a boundary commit (the root, 92 | /// or the commit specified in git_blame_options.oldest_commit) 93 | boundary: u8, 94 | 95 | pub fn origPath(self: BlameHunk) [:0]const u8 { 96 | return std.mem.sliceTo(self.z_orig_path, 0); 97 | } 98 | 99 | test { 100 | try std.testing.expectEqual(@sizeOf(c.git_blame_hunk), @sizeOf(BlameHunk)); 101 | try std.testing.expectEqual(@bitSizeOf(c.git_blame_hunk), @bitSizeOf(BlameHunk)); 102 | } 103 | 104 | comptime { 105 | std.testing.refAllDecls(@This()); 106 | } 107 | }; 108 | 109 | pub const BlameOptions = struct { 110 | flags: BlameFlags = .{}, 111 | 112 | /// The lower bound on the number of alphanumeric characters that must be detected as moving/copying within a file for it 113 | /// to associate those lines with the parent commit. The default value is 20. 114 | /// 115 | /// This value only takes effect if any of the `BlameFlags.track_copies_*` flags are specified. 116 | min_match_characters: u16 = 0, 117 | 118 | /// The id of the newest commit to consider. The default is HEAD. 119 | newest_commit: git.Oid = git.Oid.zero, 120 | 121 | /// The id of the oldest commit to consider. The default is the first commit encountered with a `null` parent. 122 | oldest_commit: git.Oid = git.Oid.zero, 123 | 124 | /// The first line in the file to blame. The default is 1 (line numbers start with 1). 125 | min_line: usize = 0, 126 | 127 | /// The last line in the file to blame. The default is the last line of the file. 128 | max_line: usize = 0, 129 | 130 | pub const BlameFlags = packed struct { 131 | normal: bool = false, 132 | 133 | /// Track lines that have moved within a file (like `git blame -M`). 134 | /// 135 | /// This is not yet implemented and reserved for future use. 136 | track_copies_same_file: bool = false, 137 | 138 | /// Track lines that have moved across files in the same commit (like `git blame -C`). 139 | /// 140 | /// This is not yet implemented and reserved for future use. 141 | track_copies_same_commit_moves: bool = false, 142 | 143 | /// Track lines that have been copied from another file that exists in the same commit (like `git blame -CC`). 144 | /// Implies same_file. 145 | /// 146 | /// This is not yet implemented and reserved for future use. 147 | track_copies_same_commit_copies: bool = false, 148 | 149 | /// Track lines that have been copied from another file that exists in *any* commit (like `git blame -CCC`). Implies 150 | /// same_commit_copies. 151 | /// 152 | /// This is not yet implemented and reserved for future use. 153 | track_copies_any_commit_copies: bool = false, 154 | 155 | /// Restrict the search of commits to those reachable following only the first parents. 156 | first_parent: bool = false, 157 | 158 | /// Use mailmap file to map author and committer names and email addresses to canonical real names and email 159 | /// addresses. The mailmap will be read from the working directory, or HEAD in a bare repository. 160 | use_mailmap: bool = false, 161 | 162 | /// Ignore whitespace differences 163 | ignore_whitespace: bool = false, 164 | 165 | z_padding1: u8 = 0, 166 | z_padding2: u16 = 0, 167 | 168 | pub fn format( 169 | value: BlameFlags, 170 | comptime fmt: []const u8, 171 | options: std.fmt.FormatOptions, 172 | writer: anytype, 173 | ) !void { 174 | _ = fmt; 175 | return internal.formatWithoutFields( 176 | value, 177 | options, 178 | writer, 179 | &.{ "z_padding1", "z_padding2" }, 180 | ); 181 | } 182 | 183 | test { 184 | try std.testing.expectEqual(@sizeOf(c.git_blame_flag_t), @sizeOf(BlameFlags)); 185 | try std.testing.expectEqual(@bitSizeOf(c.git_blame_flag_t), @bitSizeOf(BlameFlags)); 186 | } 187 | 188 | comptime { 189 | std.testing.refAllDecls(@This()); 190 | } 191 | }; 192 | 193 | comptime { 194 | std.testing.refAllDecls(@This()); 195 | } 196 | }; 197 | 198 | comptime { 199 | std.testing.refAllDecls(@This()); 200 | } 201 | -------------------------------------------------------------------------------- /src/blob.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Blob = opaque { 9 | pub fn deinit(self: *Blob) void { 10 | if (internal.trace_log) log.debug("Blob.deinit called", .{}); 11 | 12 | c.git_blob_free(@ptrCast(self)); 13 | } 14 | 15 | pub fn id(self: *const Blob) *const git.Oid { 16 | if (internal.trace_log) log.debug("Blame.id called", .{}); 17 | 18 | return @ptrCast(c.git_blob_id(@ptrCast(self))); 19 | } 20 | 21 | /// Directly generate a patch from the difference between two blobs. 22 | /// 23 | /// This is just like `Diff.blobs` except it generates a patch object for the difference instead of directly making callbacks. 24 | /// You can use the standard `Patch` accessor functions to read the patch data, and you must call `Patch.deinit on the patch 25 | /// when done. 26 | /// 27 | /// ## Parameters 28 | /// * `old` - Blob for old side of diff, or `null` for empty blob 29 | /// * `old_as_path` - Treat old blob as if it had this filename; can be `null` 30 | /// * `new` - Blob for new side of diff, or `null` for empty blob 31 | /// * `new_as_path` - Treat new blob as if it had this filename; can be `null` 32 | /// * `options` - Options for diff. 33 | pub fn toPatch( 34 | old: ?*const Blob, 35 | old_as_path: ?[:0]const u8, 36 | new: ?*const Blob, 37 | new_as_path: ?[:0]const u8, 38 | options: git.DiffOptions, 39 | ) !*git.Patch { 40 | if (internal.trace_log) log.debug("Blob.toPatch called", .{}); 41 | 42 | var ret: *git.Patch = undefined; 43 | 44 | const c_old_as_path = if (old_as_path) |s| s.ptr else null; 45 | const c_new_as_path = if (new_as_path) |s| s.ptr else null; 46 | const c_options = internal.make_c_option.diffOptions(options); 47 | 48 | try internal.wrapCall("git_patch_from_blobs", .{ 49 | @as(*?*c.git_patch, @ptrCast(&ret)), 50 | @as(?*const c.git_blob, @ptrCast(old)), 51 | c_old_as_path, 52 | @as(?*const c.git_blob, @ptrCast(new)), 53 | c_new_as_path, 54 | &c_options, 55 | }); 56 | 57 | return ret; 58 | } 59 | 60 | /// Directly generate a patch from the difference between a blob and a buffer. 61 | /// 62 | /// This is just like `Diff.blobs` except it generates a patch object for the difference instead of directly making callbacks. 63 | /// You can use the standard `Patch` accessor functions to read the patch data, and you must call `Patch.deinit on the patch 64 | /// when done. 65 | /// 66 | /// ## Parameters 67 | /// * `old` - Blob for old side of diff, or `null` for empty blob 68 | /// * `old_as_path` - Treat old blob as if it had this filename; can be `null` 69 | /// * `buffer` - Raw data for new side of diff, or `null` for empty 70 | /// * `buffer_as_path` - Treat buffer as if it had this filename; can be `null` 71 | /// * `options` - Options for diff. 72 | pub fn patchFromBuffer( 73 | old: ?*const git.Blob, 74 | old_as_path: ?[:0]const u8, 75 | buffer: ?[]const u8, 76 | buffer_as_path: ?[:0]const u8, 77 | options: git.DiffOptions, 78 | ) !*git.Patch { 79 | if (internal.trace_log) log.debug("Blob.patchFromBuffer called", .{}); 80 | 81 | var ret: *git.Patch = undefined; 82 | 83 | const c_old_as_path = if (old_as_path) |s| s.ptr else null; 84 | const c_buffer_as_path = if (buffer_as_path) |s| s.ptr else null; 85 | const c_options = internal.make_c_option.diffOptions(options); 86 | 87 | var buffer_ptr: ?[*]const u8 = null; 88 | var buffer_len: usize = 0; 89 | 90 | if (buffer) |b| { 91 | buffer_ptr = b.ptr; 92 | buffer_len = b.len; 93 | } 94 | 95 | try internal.wrapCall("git_patch_from_blob_and_buffer", .{ 96 | @as(*?*c.git_patch, @ptrCast(&ret)), 97 | @as(?*const c.git_blob, @ptrCast(old)), 98 | c_old_as_path, 99 | buffer_ptr, 100 | buffer_len, 101 | c_buffer_as_path, 102 | &c_options, 103 | }); 104 | 105 | return ret; 106 | } 107 | 108 | pub fn owner(self: *const Blob) *git.Repository { 109 | if (internal.trace_log) log.debug("Blame.owner called", .{}); 110 | 111 | return @ptrCast(c.git_blob_owner(@ptrCast(self))); 112 | } 113 | 114 | pub fn rawContent(self: *const Blob) ?*const anyopaque { 115 | if (internal.trace_log) log.debug("Blame.rawContent called", .{}); 116 | 117 | return c.git_blob_rawcontent(@ptrCast(self)); 118 | } 119 | 120 | pub fn rawContentLength(self: *const Blob) u64 { 121 | if (internal.trace_log) log.debug("Blame.rawContentLength called", .{}); 122 | 123 | return c.git_blob_rawsize(@ptrCast(self)); 124 | } 125 | 126 | pub fn isBinary(self: *const Blob) bool { 127 | if (internal.trace_log) log.debug("Blame.isBinary called", .{}); 128 | 129 | return c.git_blob_is_binary(@ptrCast(self)) == 1; 130 | } 131 | 132 | pub fn copy(self: *Blob) !*Blob { 133 | if (internal.trace_log) log.debug("Blame.copy called", .{}); 134 | 135 | var new_blob: *Blob = undefined; 136 | 137 | const ret = c.git_blob_dup(@ptrCast(&new_blob), @ptrCast(self)); 138 | // This always returns 0 139 | std.debug.assert(ret == 0); 140 | 141 | return new_blob; 142 | } 143 | 144 | pub fn filter(self: *Blob, as_path: [:0]const u8, options: BlobFilterOptions) !git.Buf { 145 | if (internal.trace_log) log.debug("Blob.filter called", .{}); 146 | 147 | var buf: git.Buf = .{}; 148 | 149 | var c_options = internal.make_c_option.blobFilterOptions(options); 150 | 151 | try internal.wrapCall("git_blob_filter", .{ 152 | @as(*c.git_buf, @ptrCast(&buf)), 153 | @as(*c.git_blob, @ptrCast(self)), 154 | as_path.ptr, 155 | &c_options, 156 | }); 157 | 158 | return buf; 159 | } 160 | 161 | comptime { 162 | std.testing.refAllDecls(@This()); 163 | } 164 | }; 165 | 166 | pub const BlobFilterOptions = struct { 167 | flags: BlobFilterFlags = .{}, 168 | /// The commit to load attributes from, when `FilterFlags.attributes_from_commit` is specified. 169 | commit_id: ?*git.Oid = null, 170 | 171 | pub const BlobFilterFlags = packed struct { 172 | /// When set, filters will not be applied to binary files. 173 | check_for_binary: bool = false, 174 | 175 | /// When set, filters will not load configuration from the system-wide `gitattributes` in `/etc` (or system equivalent). 176 | no_system_attributes: bool = false, 177 | 178 | /// When set, filters will be loaded from a `.gitattributes` file in the HEAD commit. 179 | attributes_from_head: bool = false, 180 | 181 | /// When set, filters will be loaded from a `.gitattributes` file in the specified commit. 182 | attributes_from_commit: bool = false, 183 | 184 | z_padding: u28 = 0, 185 | 186 | pub fn format( 187 | value: BlobFilterFlags, 188 | comptime fmt: []const u8, 189 | options: std.fmt.FormatOptions, 190 | writer: anytype, 191 | ) !void { 192 | _ = fmt; 193 | return internal.formatWithoutFields( 194 | value, 195 | options, 196 | writer, 197 | &.{"z_padding"}, 198 | ); 199 | } 200 | 201 | test { 202 | try std.testing.expectEqual(@sizeOf(c.git_blob_filter_flag_t), @sizeOf(BlobFilterFlags)); 203 | try std.testing.expectEqual(@bitSizeOf(c.git_blob_filter_flag_t), @bitSizeOf(BlobFilterFlags)); 204 | } 205 | 206 | comptime { 207 | std.testing.refAllDecls(@This()); 208 | } 209 | }; 210 | 211 | comptime { 212 | std.testing.refAllDecls(@This()); 213 | } 214 | }; 215 | 216 | comptime { 217 | std.testing.refAllDecls(@This()); 218 | } 219 | -------------------------------------------------------------------------------- /src/filter.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Filters are applied in one of two directions: smudging - which is exporting a file from the Git object database to the working 9 | /// directory, and cleaning - which is importing a file from the working directory to the Git object database. These values 10 | /// control which direction of change is being applied. 11 | pub const FilterMode = enum(c_uint) { 12 | to_worktree = 0, 13 | to_odb = 1, 14 | 15 | pub const smudge = FilterMode.to_worktree; 16 | pub const clean = FilterMode.to_odb; 17 | }; 18 | 19 | pub const FilterFlags = packed struct { 20 | /// Don't error for `safecrlf` violations, allow them to continue. 21 | allow_unsafe: bool = false, 22 | 23 | /// Don't load `/etc/gitattributes` (or the system equivalent) 24 | no_system_attributes: bool = false, 25 | 26 | /// Load attributes from `.gitattributes` in the root of HEAD 27 | attributes_from_head: bool = false, 28 | 29 | /// Load attributes from `.gitattributes` in a given commit. This can only be specified in a `FilterOptions` 30 | attributes_from_commit: bool = false, 31 | 32 | z_padding: u28 = 0, 33 | 34 | pub fn format( 35 | value: FilterFlags, 36 | comptime fmt: []const u8, 37 | options: std.fmt.FormatOptions, 38 | writer: anytype, 39 | ) !void { 40 | _ = fmt; 41 | return internal.formatWithoutFields( 42 | value, 43 | options, 44 | writer, 45 | &.{"z_padding"}, 46 | ); 47 | } 48 | 49 | test { 50 | try std.testing.expectEqual(@sizeOf(c.git_filter_flag_t), @sizeOf(FilterFlags)); 51 | try std.testing.expectEqual(@bitSizeOf(c.git_filter_flag_t), @bitSizeOf(FilterFlags)); 52 | } 53 | 54 | comptime { 55 | std.testing.refAllDecls(@This()); 56 | } 57 | }; 58 | 59 | pub const FilterOptions = struct { 60 | flags: FilterFlags = .{}, 61 | 62 | /// The commit to load attributes from, when `FilterFlags.attributes_from_commit` is specified. 63 | commit_id: ?*git.Oid = null, 64 | 65 | comptime { 66 | std.testing.refAllDecls(@This()); 67 | } 68 | }; 69 | 70 | /// A filter that can transform file data 71 | /// 72 | /// This represents a filter that can be used to transform or even replace file data. 73 | /// Libgit2 includes one built in filter and it is possible to write your own (see git2/sys/filter.h for information on that). 74 | /// 75 | /// The two builtin filters are: 76 | /// 77 | /// - "crlf" which uses the complex rules with the "text", "eol", and "crlf" file attributes to decide how to convert between LF 78 | /// and CRLF line endings 79 | /// - "ident" which replaces "$Id$" in a blob with "$Id: $" upon checkout and replaced "$Id: $" with "$Id$" on 80 | /// checkin. 81 | pub const Filter = opaque { 82 | comptime { 83 | std.testing.refAllDecls(@This()); 84 | } 85 | }; 86 | 87 | /// List of filters to be applied 88 | /// 89 | /// This represents a list of filters to be applied to a file / blob. You can build the list with one call, apply it with another, 90 | /// and dispose it with a third. In typical usage, there are not many occasions where a `FilterList` is needed directly since the 91 | /// library will generally handle conversions for you, but it can be convenient to be able to build and apply the list sometimes. 92 | pub const FilterList = opaque { 93 | /// Query the filter list to see if a given filter (by name) will run. 94 | /// The built-in filters "crlf" and "ident" can be queried, otherwise this is the name of the filter specified by the filter 95 | /// attribute. 96 | /// 97 | /// ## Parameters 98 | /// * `name` - The name of the filter to query 99 | pub fn contains(self: *FilterList, name: [:0]const u8) bool { 100 | if (internal.trace_log) log.debug("FilterList.contains called", .{}); 101 | 102 | return c.git_filter_list_contains(@as(*c.git_filter_list, @ptrCast(self)), name.ptr) != 0; 103 | } 104 | 105 | /// Apply a filter list to the contents of a file on disk 106 | /// 107 | /// ## Parameters 108 | /// * `repo` - The repository in which to perform the filtering 109 | /// * `path` - The path of the file to filter, a relative path will be taken as relative to the workdir 110 | pub fn applyToFile(self: *FilterList, repo: *git.Repository, path: [:0]const u8) !git.Buf { 111 | if (internal.trace_log) log.debug("FilterList.applyToFile called", .{}); 112 | 113 | var ret: git.Buf = .{}; 114 | 115 | try internal.wrapCall("git_filter_list_apply_to_file", .{ 116 | @as(*c.git_buf, @ptrCast(&ret)), 117 | @as(*c.git_filter_list, @ptrCast(self)), 118 | @as(*c.git_repository, @ptrCast(repo)), 119 | path.ptr, 120 | }); 121 | 122 | return ret; 123 | } 124 | 125 | /// Apply a filter list to the contents of a blob 126 | /// 127 | /// ## Parameters 128 | /// * `blob` - The blob to filter 129 | pub fn applyToBlob(self: *FilterList, blob: *git.Blob) !git.Buf { 130 | if (internal.trace_log) log.debug("FilterList.applyToBlob called", .{}); 131 | 132 | var ret: git.Buf = .{}; 133 | 134 | try internal.wrapCall("git_filter_list_apply_to_blob", .{ 135 | @as(*c.git_buf, @ptrCast(&ret)), 136 | @as(*c.git_filter_list, @ptrCast(self)), 137 | @as(*c.git_blob, @ptrCast(blob)), 138 | }); 139 | 140 | return ret; 141 | } 142 | 143 | /// Apply a filter list to a file as a stream 144 | /// 145 | /// ## Parameters 146 | /// * `repo` - The repository in which to perform the filtering 147 | /// * `path` - The path of the file to filter, a relative path will be taken as relative to the workdir 148 | /// * `target` - The stream into which the data will be written 149 | pub fn applyToFileToStream( 150 | self: *FilterList, 151 | repo: *git.Repository, 152 | path: [:0]const u8, 153 | target: *git.WriteStream, 154 | ) !void { 155 | if (internal.trace_log) log.debug("FilterList.applyToFileToStream called", .{}); 156 | 157 | try internal.wrapCall("git_filter_list_stream_file", .{ 158 | @as(*c.git_filter_list, @ptrCast(self)), 159 | @as(*c.git_repository, @ptrCast(repo)), 160 | path.ptr, 161 | @as(*c.git_writestream, @ptrCast(target)), 162 | }); 163 | } 164 | 165 | /// Apply a filter list to a blob as a stream 166 | /// 167 | /// ## Parameters 168 | /// * `blob` - The blob to filter 169 | /// * `target` - The stream into which the data will be written 170 | pub fn applyToBlobToStream(self: *FilterList, blob: *git.Blob, target: *git.WriteStream) !void { 171 | if (internal.trace_log) log.debug("FilterList.applyToBlobToStream called", .{}); 172 | 173 | try internal.wrapCall("git_filter_list_stream_blob", .{ 174 | @as(*c.git_filter_list, @ptrCast(self)), 175 | @as(*c.git_blob, @ptrCast(blob)), 176 | @as(*c.git_writestream, @ptrCast(target)), 177 | }); 178 | } 179 | 180 | pub fn deinit(self: *FilterList) void { 181 | if (internal.trace_log) log.debug("FilterList.deinit called", .{}); 182 | 183 | c.git_filter_list_free(@as(*c.git_filter_list, @ptrCast(self))); 184 | } 185 | 186 | /// Apply filter list to a data buffer. 187 | /// 188 | /// ## Parameters 189 | /// * `name` - Buffer containing the data to filter 190 | pub fn applyToBuffer(self: *FilterList, in: [:0]const u8) !git.Buf { 191 | if (internal.trace_log) log.debug("FilterList.applyToBuffer called", .{}); 192 | 193 | var ret: git.Buf = .{}; 194 | 195 | try internal.wrapCall("git_filter_list_apply_to_buffer", .{ 196 | @as(*c.git_buf, @ptrCast(&ret)), 197 | @as(*c.git_filter_list, @ptrCast(self)), 198 | in.ptr, 199 | in.len, 200 | }); 201 | 202 | return ret; 203 | } 204 | 205 | /// Apply a filter list to an arbitrary buffer as a stream 206 | /// 207 | /// ## Parameters 208 | /// * `buffer` - The buffer to filter 209 | /// * `target` - The stream into which the data will be written 210 | pub fn applyToBufferToStream(self: *FilterList, buffer: [:0]const u8, target: *git.WriteStream) !void { 211 | if (internal.trace_log) log.debug("FilterList.applyToBufferToStream called", .{}); 212 | 213 | try internal.wrapCall("git_filter_list_stream_buffer", .{ 214 | @as(*c.git_filter_list, @ptrCast(self)), 215 | buffer.ptr, 216 | buffer.len, 217 | @as(*c.git_writestream, @ptrCast(target)), 218 | }); 219 | } 220 | 221 | comptime { 222 | std.testing.refAllDecls(@This()); 223 | } 224 | }; 225 | 226 | comptime { 227 | std.testing.refAllDecls(@This()); 228 | } 229 | -------------------------------------------------------------------------------- /src/credential.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Credential = extern struct { 9 | credtype: CredentialType, 10 | free: *const fn (*Credential) callconv(.C) void, 11 | 12 | pub fn deinit(self: *Credential) void { 13 | if (internal.trace_log) log.debug("Credential.deinit called", .{}); 14 | 15 | if (internal.has_credential) { 16 | c.git_credential_free(@as(*internal.RawCredentialType, @ptrCast(self))); 17 | } else { 18 | c.git_cred_free(@as(*internal.RawCredentialType, @ptrCast(self))); 19 | } 20 | } 21 | 22 | pub fn hasUsername(self: *Credential) bool { 23 | if (internal.trace_log) log.debug("Credential.hasUsername called", .{}); 24 | 25 | return if (internal.has_credential) 26 | c.git_credential_has_username(@as(*internal.RawCredentialType, @ptrCast(self))) != 0 27 | else 28 | c.git_cred_has_username(@as(*internal.RawCredentialType, @ptrCast(self))) != 0; 29 | } 30 | 31 | pub fn getUsername(self: *Credential) ?[:0]const u8 { 32 | if (internal.trace_log) log.debug("Credential.getUsername called", .{}); 33 | 34 | const opt_username = if (internal.has_credential) 35 | c.git_credential_get_username(@as(*internal.RawCredentialType, @ptrCast(self))) 36 | else 37 | c.git_cred_get_username(@as(*internal.RawCredentialType, @ptrCast(self))); 38 | 39 | return if (opt_username) |username| std.mem.sliceTo(username, 0) else null; 40 | } 41 | 42 | /// A plaintext username and password 43 | pub const CredentialUserpassPlaintext = extern struct { 44 | /// The parent credential 45 | parent: Credential, 46 | /// The username to authenticate as 47 | username: [*:0]const u8, 48 | /// The password to use 49 | password: [*:0]const u8, 50 | 51 | test { 52 | try std.testing.expectEqual(@sizeOf(c.git_credential_userpass_plaintext), @sizeOf(CredentialUserpassPlaintext)); 53 | try std.testing.expectEqual(@bitSizeOf(c.git_credential_userpass_plaintext), @bitSizeOf(CredentialUserpassPlaintext)); 54 | } 55 | 56 | comptime { 57 | std.testing.refAllDecls(@This()); 58 | } 59 | }; 60 | 61 | /// Username-only credential information 62 | pub const CredentialUsername = extern struct { 63 | /// The parent credential 64 | parent: Credential, 65 | /// Use `username()` 66 | z_username: [1]u8, 67 | 68 | /// The username to authenticate as 69 | pub fn username(self: CredentialUsername) [:0]const u8 { 70 | return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&self.z_username)), 0); 71 | } 72 | 73 | test { 74 | try std.testing.expectEqual(@sizeOf(c.git_credential_username), @sizeOf(CredentialUsername)); 75 | try std.testing.expectEqual(@bitSizeOf(c.git_credential_username), @bitSizeOf(CredentialUsername)); 76 | } 77 | 78 | comptime { 79 | std.testing.refAllDecls(@This()); 80 | } 81 | }; 82 | 83 | /// A ssh key from disk 84 | pub const CredentialSshKey = extern struct { 85 | /// The parent credential 86 | parent: Credential, 87 | /// The username to authenticate as 88 | username: [*:0]const u8, 89 | /// The path to a public key 90 | publickey: [*:0]const u8, 91 | /// The path to a private key 92 | privatekey: [*:0]const u8, 93 | /// Passphrase to decrypt the private key 94 | passphrase: ?[*:0]const u8, 95 | 96 | test { 97 | try std.testing.expectEqual(@sizeOf(c.git_credential_ssh_key), @sizeOf(CredentialSshKey)); 98 | try std.testing.expectEqual(@bitSizeOf(c.git_credential_ssh_key), @bitSizeOf(CredentialSshKey)); 99 | } 100 | 101 | comptime { 102 | std.testing.refAllDecls(@This()); 103 | } 104 | }; 105 | 106 | usingnamespace if (internal.has_libssh2) struct { 107 | /// A plaintext username and password 108 | pub const CredentialSshInteractive = extern struct { 109 | /// The parent credential 110 | parent: Credential, 111 | /// The username to authenticate as 112 | username: [*:0]const u8, 113 | 114 | /// Callback used for authentication. 115 | prompt_callback: *const fn ( 116 | name: [*]const u8, 117 | name_len: c_int, 118 | instruction: [*]const u8, 119 | instruction_len: c_int, 120 | num_prompts: c_int, 121 | prompts: ?*const c.LIBSSH2_USERAUTH_KBDINT_PROMPT, 122 | responses: ?*c.LIBSSH2_USERAUTH_KBDINT_RESPONSE, 123 | abstract: ?*?*anyopaque, 124 | ) callconv(.C) void, 125 | 126 | /// Payload passed to prompt_callback 127 | payload: ?*anyopaque, 128 | 129 | test { 130 | try std.testing.expectEqual(@sizeOf(c.git_credential_ssh_interactive), @sizeOf(CredentialSshInteractive)); 131 | try std.testing.expectEqual(@bitSizeOf(c.git_credential_ssh_interactive), @bitSizeOf(CredentialSshInteractive)); 132 | } 133 | 134 | comptime { 135 | std.testing.refAllDecls(@This()); 136 | } 137 | }; 138 | 139 | /// A ssh key from disk 140 | pub const CredentialSshCustom = extern struct { 141 | /// The parent credential 142 | parent: Credential, 143 | /// The username to authenticate as 144 | username: [*:0]const u8, 145 | /// The public key data 146 | publickey: [*:0]const u8, 147 | /// Length of the public key 148 | publickey_len: usize, 149 | 150 | sign_callback: *const fn ( 151 | session: ?*c.LIBSSH2_SESSION, 152 | sig: *[*:0]u8, 153 | sig_len: *usize, 154 | data: [*]const u8, 155 | data_len: usize, 156 | abstract: ?*?*anyopaque, 157 | ) callconv(.C) c_int, 158 | 159 | payload: ?*anyopaque, 160 | 161 | test { 162 | try std.testing.expectEqual(@sizeOf(c.git_credential_ssh_custom), @sizeOf(CredentialSshCustom)); 163 | try std.testing.expectEqual(@bitSizeOf(c.git_credential_ssh_custom), @bitSizeOf(CredentialSshCustom)); 164 | } 165 | 166 | comptime { 167 | std.testing.refAllDecls(@This()); 168 | } 169 | }; 170 | 171 | comptime { 172 | std.testing.refAllDecls(@This()); 173 | } 174 | } else struct {}; 175 | 176 | test { 177 | try std.testing.expectEqual(@sizeOf(c.git_cred), @sizeOf(Credential)); 178 | try std.testing.expectEqual(@bitSizeOf(c.git_cred), @bitSizeOf(Credential)); 179 | } 180 | 181 | comptime { 182 | std.testing.refAllDecls(@This()); 183 | } 184 | }; 185 | 186 | pub const CredentialType = packed struct { 187 | /// A vanilla user/password request 188 | userpass_plaintext: bool = false, 189 | 190 | /// An SSH key-based authentication request 191 | ssh_key: bool = false, 192 | 193 | /// An SSH key-based authentication request, with a custom signature 194 | ssh_custom: bool = false, 195 | 196 | /// An NTLM/Negotiate-based authentication request. 197 | default: bool = false, 198 | 199 | /// An SSH interactive authentication request 200 | ssh_interactive: bool = false, 201 | 202 | /// Username-only authentication request 203 | /// 204 | /// Used as a pre-authentication step if the underlying transport (eg. SSH, with no username in its URL) does not know 205 | /// which username to use. 206 | username: bool = false, 207 | 208 | /// An SSH key-based authentication request 209 | /// 210 | /// Allows credentials to be read from memory instead of files. 211 | /// Note that because of differences in crypto backend support, it might not be functional. 212 | ssh_memory: bool = false, 213 | 214 | z_padding: u25 = 0, 215 | 216 | pub fn format( 217 | value: CredentialType, 218 | comptime fmt: []const u8, 219 | options: std.fmt.FormatOptions, 220 | writer: anytype, 221 | ) !void { 222 | _ = fmt; 223 | return internal.formatWithoutFields( 224 | value, 225 | options, 226 | writer, 227 | &.{"z_padding"}, 228 | ); 229 | } 230 | 231 | test { 232 | try std.testing.expectEqual(@sizeOf(c_uint), @sizeOf(CredentialType)); 233 | try std.testing.expectEqual(@bitSizeOf(c_uint), @bitSizeOf(CredentialType)); 234 | } 235 | 236 | comptime { 237 | std.testing.refAllDecls(@This()); 238 | } 239 | }; 240 | 241 | comptime { 242 | std.testing.refAllDecls(@This()); 243 | } 244 | -------------------------------------------------------------------------------- /src/git.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | pub const path_list_separator = c.GIT_PATH_LIST_SEPARATOR; 7 | 8 | const alloc = @import("alloc.zig"); 9 | pub const GitAllocator = alloc.GitAllocator; 10 | 11 | const annotated_commit = @import("annotated_commit.zig"); 12 | pub const AnnotatedCommit = annotated_commit.AnnotatedCommit; 13 | 14 | const attribute = @import("attribute.zig"); 15 | pub const Attribute = attribute.Attribute; 16 | pub const AttributeOptions = attribute.AttributeOptions; 17 | pub const AttributeFlags = attribute.AttributeFlags; 18 | 19 | const blame = @import("blame.zig"); 20 | pub const Blame = blame.Blame; 21 | pub const BlameHunk = blame.BlameHunk; 22 | pub const BlameOptions = blame.BlameOptions; 23 | 24 | const blob = @import("blob.zig"); 25 | pub const Blob = blob.Blob; 26 | pub const BlobFilterOptions = blob.BlobFilterOptions; 27 | 28 | const buffer = @import("buffer.zig"); 29 | pub const Buf = buffer.Buf; 30 | 31 | const certificate = @import("certificate.zig"); 32 | pub const Certificate = certificate.Certificate; 33 | 34 | const commit = @import("commit.zig"); 35 | pub const Commit = commit.Commit; 36 | pub const RevertOptions = commit.RevertOptions; 37 | 38 | const config = @import("config.zig"); 39 | pub const Config = config.Config; 40 | pub const ConfigBackend = config.ConfigBackend; 41 | pub const ConfigLevel = config.ConfigLevel; 42 | 43 | const credential = @import("credential.zig"); 44 | pub const Credential = credential.Credential; 45 | pub const CredentialType = credential.CredentialType; 46 | 47 | const describe = @import("describe.zig"); 48 | pub const DescribeOptions = describe.DescribeOptions; 49 | pub const DescribeFormatOptions = describe.DescribeFormatOptions; 50 | pub const DescribeResult = describe.DescribeResult; 51 | 52 | const diff = @import("diff.zig"); 53 | pub const Diff = diff.Diff; 54 | pub const DiffLine = diff.DiffLine; 55 | pub const DiffOptions = diff.DiffOptions; 56 | pub const DiffPerfData = diff.DiffPerfData; 57 | pub const DiffHunk = diff.DiffHunk; 58 | pub const DiffDelta = diff.DiffDelta; 59 | pub const ApplyOptions = diff.ApplyOptions; 60 | pub const ApplyOptionsWithUserData = diff.ApplyOptionsWithUserData; 61 | pub const ApplyOptionsFlags = diff.ApplyOptionsFlags; 62 | pub const ApplyLocation = diff.ApplyLocation; 63 | pub const DiffFile = diff.DiffFile; 64 | pub const DiffFlags = diff.DiffFlags; 65 | 66 | const errors = @import("errors.zig"); 67 | pub const GitError = errors.GitError; 68 | pub const errorToCInt = errors.errorToCInt; 69 | pub const DetailedError = errors.DetailedError; 70 | 71 | const filter = @import("filter.zig"); 72 | pub const FilterMode = filter.FilterMode; 73 | pub const FilterFlags = filter.FilterFlags; 74 | pub const FilterOptions = filter.FilterOptions; 75 | pub const Filter = filter.Filter; 76 | pub const FilterList = filter.FilterList; 77 | 78 | const handle = @import("handle.zig"); 79 | pub const Handle = handle.Handle; 80 | pub const CloneOptions = handle.CloneOptions; 81 | 82 | const hashsig = @import("hashsig.zig"); 83 | pub const Hashsig = hashsig.Hashsig; 84 | pub const HashsigOptions = hashsig.HashsigOptions; 85 | 86 | const index = @import("index.zig"); 87 | pub const Index = index.Index; 88 | pub const IndexAddFlags = index.IndexAddFlags; 89 | 90 | const indexer = @import("indexer.zig"); 91 | pub const Indexer = indexer.Indexer; 92 | pub const IndexerProgress = indexer.IndexerProgress; 93 | pub const IndexerOptions = indexer.IndexerOptions; 94 | 95 | const mailmap = @import("mailmap.zig"); 96 | pub const Mailmap = mailmap.Mailmap; 97 | 98 | const merge = @import("merge.zig"); 99 | pub const SimilarityMetric = merge.SimilarityMetric; 100 | pub const FileFavor = merge.FileFavor; 101 | pub const MergeOptions = merge.MergeOptions; 102 | 103 | const message = @import("message.zig"); 104 | pub const MessageTrailerArray = message.MessageTrailerArray; 105 | 106 | const net = @import("net.zig"); 107 | pub const Direction = net.Direction; 108 | 109 | const notes = @import("notes.zig"); 110 | pub const Note = notes.Note; 111 | pub const NoteIterator = notes.NoteIterator; 112 | 113 | const object = @import("object.zig"); 114 | pub const Object = object.Object; 115 | pub const ObjectType = object.ObjectType; 116 | 117 | const odb = @import("odb.zig"); 118 | pub const Odb = odb.Odb; 119 | 120 | const oid = @import("oid.zig"); 121 | pub const Oid = oid.Oid; 122 | pub const OidShortener = oid.OidShortener; 123 | 124 | const oidarray = @import("oidarray.zig"); 125 | pub const OidArray = oidarray.OidArray; 126 | 127 | const pack = @import("pack.zig"); 128 | pub const PackBuilder = pack.PackBuilder; 129 | pub const PackbuilderStage = pack.PackbuilderStage; 130 | 131 | const patch = @import("patch.zig"); 132 | pub const Patch = patch.Patch; 133 | 134 | const pathspec = @import("pathspec.zig"); 135 | pub const Pathspec = pathspec.Pathspec; 136 | pub const PathspecMatchOptions = pathspec.PathspecMatchOptions; 137 | pub const PathspecMatchList = pathspec.PathspecMatchList; 138 | 139 | const proxy = @import("proxy.zig"); 140 | pub const ProxyOptions = proxy.ProxyOptions; 141 | 142 | const rebase = @import("rebase.zig"); 143 | pub const Rebase = rebase.Rebase; 144 | pub const RebaseOperation = rebase.RebaseOperation; 145 | pub const RebaseOptions = rebase.RebaseOptions; 146 | 147 | const refdb = @import("refdb.zig"); 148 | pub const Refdb = refdb.Refdb; 149 | 150 | const reference = @import("reference.zig"); 151 | pub const Reference = reference.Reference; 152 | 153 | const reflog = @import("reflog.zig"); 154 | pub const Reflog = reflog.Reflog; 155 | 156 | const refspec = @import("refspec.zig"); 157 | pub const Refspec = refspec.Refspec; 158 | 159 | const remote = @import("remote.zig"); 160 | pub const Remote = remote.Remote; 161 | pub const RemoteAutoTagOption = remote.RemoteAutoTagOption; 162 | pub const RemoteCreateOptions = remote.RemoteCreateOptions; 163 | pub const FetchOptions = remote.FetchOptions; 164 | pub const PushOptions = remote.PushOptions; 165 | pub const RemoteCallbacks = remote.RemoteCallbacks; 166 | 167 | const repository = @import("repository.zig"); 168 | pub const Repository = repository.Repository; 169 | pub const StashApplyOptions = repository.StashApplyOptions; 170 | pub const StashApplyProgress = repository.StashApplyProgress; 171 | pub const StashFlags = repository.StashFlags; 172 | pub const ResetType = repository.ResetType; 173 | pub const RepositoryInitOptions = repository.RepositoryInitOptions; 174 | pub const FileStatusOptions = repository.FileStatusOptions; 175 | pub const RepositoryOpenOptions = repository.RepositoryOpenOptions; 176 | pub const FileMode = repository.FileMode; 177 | pub const FileStatus = repository.FileStatus; 178 | pub const RevSpec = repository.RevSpec; 179 | pub const CheckoutOptions = repository.CheckoutOptions; 180 | pub const CherrypickOptions = repository.CherrypickOptions; 181 | 182 | const revwalk = @import("revwalk.zig"); 183 | pub const RevWalk = revwalk.RevWalk; 184 | 185 | const signature = @import("signature.zig"); 186 | pub const Signature = signature.Signature; 187 | pub const Time = signature.Time; 188 | 189 | const status_list = @import("status_list.zig"); 190 | pub const StatusList = status_list.StatusList; 191 | 192 | const strarray = @import("strarray.zig"); 193 | pub const StrArray = strarray.StrArray; 194 | 195 | const submodule = @import("submodule.zig"); 196 | pub const SubmoduleIgnore = submodule.SubmoduleIgnore; 197 | 198 | const tag = @import("tag.zig"); 199 | pub const Tag = tag.Tag; 200 | 201 | const transaction = @import("transaction.zig"); 202 | pub const Transaction = transaction.Transaction; 203 | 204 | const transport = @import("transport.zig"); 205 | pub const Transport = transport.Transport; 206 | 207 | const tree = @import("tree.zig"); 208 | pub const Tree = tree.Tree; 209 | pub const TreeEntry = tree.TreeEntry; 210 | pub const TreeBuilder = tree.TreeBuilder; 211 | 212 | const worktree = @import("worktree.zig"); 213 | pub const Worktree = worktree.Worktree; 214 | pub const WorktreeAddOptions = worktree.WorktreeAddOptions; 215 | pub const PruneOptions = worktree.PruneOptions; 216 | 217 | const writestream = @import("writestream.zig"); 218 | pub const WriteStream = writestream.WriteStream; 219 | 220 | const git = @This(); 221 | 222 | /// Initialize global state. This function must be called before any other function. 223 | /// *NOTE*: This function can called multiple times. 224 | pub fn init() !git.Handle { 225 | if (internal.trace_log) log.debug("init called", .{}); 226 | 227 | const number = try internal.wrapCallWithReturn("git_libgit2_init", .{}); 228 | 229 | if (number > 1) { 230 | if (internal.trace_log) log.debug("{} ongoing initalizations without shutdown", .{number}); 231 | } 232 | 233 | return git.Handle{}; 234 | } 235 | 236 | pub fn availableLibGit2Features() LibGit2Features { 237 | return @as(LibGit2Features, @bitCast(c.git_libgit2_features())); 238 | } 239 | 240 | pub const LibGit2Features = packed struct { 241 | /// If set, libgit2 was built thread-aware and can be safely used from multiple threads. 242 | threads: bool = false, 243 | /// If set, libgit2 was built with and linked against a TLS implementation. 244 | /// Custom TLS streams may still be added by the user to support HTTPS regardless of this. 245 | https: bool = false, 246 | /// If set, libgit2 was built with and linked against libssh2. A custom transport may still be added by the user to support 247 | /// libssh2 regardless of this. 248 | ssh: bool = false, 249 | /// If set, libgit2 was built with support for sub-second resolution in file modification times. 250 | nsec: bool = false, 251 | 252 | z_padding: u28 = 0, 253 | 254 | pub fn format( 255 | value: LibGit2Features, 256 | comptime fmt: []const u8, 257 | options: std.fmt.FormatOptions, 258 | writer: anytype, 259 | ) !void { 260 | _ = fmt; 261 | return internal.formatWithoutFields(value, options, writer, &.{"z_padding"}); 262 | } 263 | 264 | test { 265 | try std.testing.expectEqual(@sizeOf(c_uint), @sizeOf(LibGit2Features)); 266 | try std.testing.expectEqual(@bitSizeOf(c_uint), @bitSizeOf(LibGit2Features)); 267 | } 268 | 269 | comptime { 270 | std.testing.refAllDecls(@This()); 271 | } 272 | }; 273 | 274 | comptime { 275 | std.testing.refAllDecls(@This()); 276 | } 277 | -------------------------------------------------------------------------------- /src/patch.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | /// Stores all the text diffs for a delta. 9 | /// 10 | /// You can easily loop over the content of patches and get information about them. 11 | pub const Patch = opaque { 12 | pub fn deinit(self: *Patch) void { 13 | if (internal.trace_log) log.debug("Patch.deinit called", .{}); 14 | 15 | c.git_patch_free(@as(*c.git_patch, @ptrCast(self))); 16 | } 17 | 18 | /// Get the delta associated with a patch. This delta points to internal data and you do not have to release it when you are 19 | /// done with it. 20 | pub fn getDelta(self: *const git.Patch) *const git.DiffDelta { 21 | if (internal.trace_log) log.debug("Patch.getDelta called", .{}); 22 | 23 | return @as( 24 | *const git.DiffDelta, 25 | @ptrCast(c.git_patch_get_delta(@as(*const c.git_patch, @ptrCast(self)))), 26 | ); 27 | } 28 | 29 | /// Get the number of hunks in a patch 30 | pub fn numberOfHunks(self: *const git.Patch) usize { 31 | if (internal.trace_log) log.debug("Patch.numberOfHunks called", .{}); 32 | 33 | return c.git_patch_num_hunks(@as(*const c.git_patch, @ptrCast(self))); 34 | } 35 | 36 | /// Get line counts of each type in a patch. 37 | /// 38 | /// This helps imitate a diff --numstat type of output. For that purpose, you only need the `total_additions` and 39 | /// `total_deletions` values, but we include the `total_context` line count in case you want the total number of lines of diff 40 | /// output that will be generated. 41 | /// 42 | /// All outputs are optional. Pass `nuull` if you don't need a particular count. 43 | /// 44 | /// ## Parameters 45 | /// * `total_context` - Count of context lines in output, can be `null`. 46 | /// * `total_additions` - Count of addition lines in output, can be `null`. 47 | /// * `total_deletions` - Count of deletion lines in output, can be `null`. 48 | pub fn lineStats(self: *const git.Patch, total_context: ?*usize, total_additions: ?*usize, total_deletions: ?*usize) !void { 49 | if (internal.trace_log) log.debug("Blob.lineStats called", .{}); 50 | 51 | try internal.wrapCall("git_patch_line_stats", .{ 52 | total_context, 53 | total_additions, 54 | total_deletions, 55 | @as(*const c.git_patch, @ptrCast(self)), 56 | }); 57 | } 58 | 59 | /// Get the information about a hunk in a patch 60 | /// 61 | /// Given a patch and a hunk index into the patch, this returns detailed information about that hunk. 62 | /// 63 | /// ## Parameters 64 | /// * `index` - Input index of hunk to get information about. 65 | pub fn getHunk(self: *git.Patch, index: usize) !GetHunkResult { 66 | if (internal.trace_log) log.debug("Blob.getHunk called", .{}); 67 | 68 | var ret: GetHunkResult = undefined; 69 | 70 | try internal.wrapCall("git_patch_get_hunk", .{ 71 | @as(*?*c.git_diff_hunk, @ptrCast(&ret.diff_hunk)), 72 | &ret.lines_in_hunk, 73 | @as(*c.git_patch, @ptrCast(self)), 74 | index, 75 | }); 76 | 77 | return ret; 78 | } 79 | 80 | pub const GetHunkResult = struct { 81 | diff_hunk: *git.DiffHunk, 82 | /// Output count of total lines in this hunk. 83 | lines_in_hunk: usize, 84 | }; 85 | 86 | /// Get the number of lines in a hunk. 87 | /// 88 | /// ## Parameters 89 | /// * `index` - Index of the hunk. 90 | pub fn linesInHunk(self: *const git.Patch, index: usize) !usize { 91 | if (internal.trace_log) log.debug("Patch.linesInHunk called", .{}); 92 | 93 | const ret = try internal.wrapCallWithReturn("git_patch_num_lines_in_hunk", .{ 94 | @as(*const c.git_patch, @ptrCast(self)), 95 | index, 96 | }); 97 | 98 | return @as(usize, @intCast(ret)); 99 | } 100 | 101 | /// Get data about a line in a hunk of a patch. 102 | /// 103 | /// Given a patch, a hunk index, and a line index in the hunk, this will return a lot of details about that line. 104 | /// 105 | /// ## Parameters 106 | /// * `hunk_index` - The index of the hunk. 107 | /// * `line` - The index of the line in the hunk. 108 | pub fn getLineInHunk(self: *Patch, hunk_index: usize, line: usize) !*git.DiffLine { 109 | if (internal.trace_log) log.debug("Patch.getLineInHunk called", .{}); 110 | 111 | var ret: *git.DiffLine = undefined; 112 | 113 | try internal.wrapCall("git_patch_get_line_in_hunk", .{ 114 | @as(*?*c.git_diff_line, @ptrCast(&ret)), 115 | @as(*c.git_patch, @ptrCast(self)), 116 | hunk_index, 117 | line, 118 | }); 119 | 120 | return ret; 121 | } 122 | 123 | /// Look up size of patch diff data in bytes 124 | /// 125 | /// This returns the raw size of the patch data.This only includes the actual data from the lines of the diff, not the file or 126 | /// hunk headers. 127 | /// 128 | /// If you pass `include_context` as true, this will be the size of all of the diff output; if you pass it as false, this will 129 | /// only include the actual changed lines. 130 | /// 131 | /// ## Parameters 132 | /// * `include_context` - Include context lines in size. 133 | /// * `include_hunk_headers` - Include hunk header lines. 134 | /// * `include_file_header` - Include file header lines. 135 | pub fn size(self: *Patch, include_context: bool, include_hunk_headers: bool, include_file_header: bool) !usize { 136 | if (internal.trace_log) log.debug("Patch.size called", .{}); 137 | 138 | const ret = c.git_patch_size( 139 | @as(*c.git_patch, @ptrCast(self)), 140 | @intFromBool(include_context), 141 | @intFromBool(include_hunk_headers), 142 | @intFromBool(include_file_header), 143 | ); 144 | 145 | return ret; 146 | } 147 | 148 | /// Serialize the patch to text via callback. 149 | /// 150 | /// Return a non-zero value from the callback to stop the loop. This non-zero value is returned by the function. 151 | /// 152 | /// ## Parameters 153 | /// * `callback_fn` - Callback function to output lines of the patch. Will be called for file headers, hunk headers, and diff 154 | /// lines. 155 | /// 156 | /// ## Callback Parameters 157 | /// * `delta` - Delta that contains this data 158 | /// * `hunk` - Hunk containing this data 159 | /// * `line` - Line data 160 | pub fn print( 161 | self: *Patch, 162 | comptime callback_fn: fn ( 163 | delta: *const git.DiffDelta, 164 | hunk: *const git.DiffHunk, 165 | line: *const git.DiffLine, 166 | ) c_int, 167 | ) !c_int { 168 | const cb = struct { 169 | pub fn cb( 170 | delta: *const git.DiffDelta, 171 | hunk: *const git.DiffHunk, 172 | line: *const git.DiffLine, 173 | _: *u8, 174 | ) c_int { 175 | return callback_fn(delta, hunk, line); 176 | } 177 | }.cb; 178 | 179 | var dummy_data: u8 = undefined; 180 | return self.printWithUserData(&dummy_data, cb); 181 | } 182 | 183 | /// Serialize the patch to text via callback. 184 | /// 185 | /// Return a non-zero value from the callback to stop the loop. This non-zero value is returned by the function. 186 | /// 187 | /// ## Parameters 188 | /// * `user_data` - Pointer to user data to be passed to the callback 189 | /// * `callback_fn` - Callback function to output lines of the patch. Will be called for file headers, hunk headers, and diff 190 | /// lines. 191 | /// 192 | /// ## Callback Parameters 193 | /// * `delta` - Delta that contains this data 194 | /// * `hunk` - Hunk containing this data 195 | /// * `line` - Line data 196 | /// * `user_data_ptr` - Pointer to user data 197 | pub fn printWithUserData( 198 | self: *Patch, 199 | user_data: anytype, 200 | comptime callback_fn: fn ( 201 | delta: *const git.DiffDelta, 202 | hunk: *const git.DiffHunk, 203 | line: *const git.DiffLine, 204 | user_data_ptr: @TypeOf(user_data), 205 | ) c_int, 206 | ) !c_int { 207 | const UserDataType = @TypeOf(user_data); 208 | 209 | const cb = struct { 210 | pub fn cb( 211 | delta: ?*const c.git_diff_delta, 212 | hunk: ?*const c.git_diff_hunk, 213 | line: ?*const c.git_diff_line, 214 | payload: ?*anyopaque, 215 | ) callconv(.C) c_int { 216 | return callback_fn( 217 | @as(*const git.DiffDelta, @ptrCast(delta)), 218 | @as(*const git.DiffHunk, @ptrCast(hunk)), 219 | @as(*const git.DiffLine, @ptrCast(line)), 220 | @as(UserDataType, @ptrCast(@alignCast( payload))), 221 | ); 222 | } 223 | }.cb; 224 | 225 | if (internal.trace_log) log.debug("Patch.printWithUserData called", .{}); 226 | 227 | return try internal.wrapCallWithReturn("git_patch_print", .{ 228 | @as(*c.git_patch, @ptrCast(self)), 229 | cb, 230 | user_data, 231 | }); 232 | } 233 | 234 | /// Get the content of a patch as a single diff text. 235 | pub fn toBuf(self: *Patch) !git.Buf { 236 | if (internal.trace_log) log.debug("Patch.toBuf called", .{}); 237 | 238 | var buf: git.Buf = .{}; 239 | 240 | try internal.wrapCall("git_patch_to_buf", .{ 241 | @as(*c.git_buf, @ptrCast(&buf)), 242 | @as(*c.git_patch, @ptrCast(self)), 243 | }); 244 | 245 | return buf; 246 | } 247 | 248 | /// Get the repository associated with this patch. 249 | pub fn getOwner(self: *const Patch) ?*git.Repository { 250 | if (internal.trace_log) log.debug("Patch.getOwner called", .{}); 251 | 252 | return @as( 253 | ?*git.Repository, 254 | @ptrCast(c.git_patch_owner(@as(*const c.git_patch, @ptrCast(self)))), 255 | ); 256 | } 257 | 258 | comptime { 259 | std.testing.refAllDecls(@This()); 260 | } 261 | }; 262 | 263 | comptime { 264 | std.testing.refAllDecls(@This()); 265 | } 266 | -------------------------------------------------------------------------------- /src/commit.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Commit = opaque { 9 | pub fn deinit(self: *Commit) void { 10 | if (internal.trace_log) log.debug("Commit.deinit called", .{}); 11 | 12 | c.git_commit_free(@ptrCast(self)); 13 | } 14 | 15 | pub fn noteIterator(self: *Commit) !*git.NoteIterator { 16 | if (internal.trace_log) log.debug("Commit.noteIterator called", .{}); 17 | 18 | var ret: *git.NoteIterator = undefined; 19 | 20 | try internal.wrapCall("git_note_commit_iterator_new", .{ 21 | @as(*?*c.git_note_iterator, @ptrCast(&ret)), 22 | @as(*c.git_commit, @ptrCast(self)), 23 | }); 24 | 25 | return ret; 26 | } 27 | 28 | pub fn id(self: *const Commit) *const git.Oid { 29 | if (internal.trace_log) log.debug("Commit.id called", .{}); 30 | 31 | return @ptrCast(c.git_commit_id(@ptrCast(self))); 32 | } 33 | 34 | pub fn getOwner(self: *const Commit) *git.Repository { 35 | if (internal.trace_log) log.debug("Commit.getOwner called", .{}); 36 | 37 | return @ptrCast(c.git_commit_owner(@ptrCast(self))); 38 | } 39 | 40 | pub fn getMessageEncoding(self: *const Commit) ?[:0]const u8 { 41 | if (internal.trace_log) log.debug("Commit.getMessageEncoding called", .{}); 42 | 43 | const ret = c.git_commit_message_encoding(@ptrCast(self)); 44 | 45 | return if (ret) |c_str| std.mem.sliceTo(c_str, 0) else null; 46 | } 47 | /// Get the full message of a commit. 48 | /// 49 | /// The returned message will be slightly prettified by removing any potential leading newlines. 50 | pub fn getMessage(self: *const Commit) ?[:0]const u8 { 51 | if (internal.trace_log) log.debug("Commit.getMessage called", .{}); 52 | 53 | const ret = c.git_commit_message(@ptrCast(self)); 54 | 55 | return if (ret) |c_str| std.mem.sliceTo(c_str, 0) else null; 56 | } 57 | 58 | /// Get the full raw message of a commit. 59 | pub fn getMessageRaw(self: *const Commit) ?[:0]const u8 { 60 | if (internal.trace_log) log.debug("Commit.getMessageRaw called", .{}); 61 | 62 | const ret = c.git_commit_message_raw(@ptrCast(self)); 63 | 64 | return if (ret) |c_str| std.mem.sliceTo(c_str, 0) else null; 65 | } 66 | 67 | /// Get the full raw text of the commit header. 68 | pub fn getHeaderRaw(self: *const Commit) ?[:0]const u8 { 69 | if (internal.trace_log) log.debug("Commit.getHeaderRaw called", .{}); 70 | 71 | const ret = c.git_commit_raw_header(@ptrCast(self)); 72 | 73 | return if (ret) |c_str| std.mem.sliceTo(c_str, 0) else null; 74 | } 75 | 76 | /// Get the short "summary" of the git commit message. 77 | /// 78 | /// The returned message is the summary of the commit, comprising the first paragraph of the message with whitespace trimmed 79 | /// and squashed. 80 | pub fn getSummary(self: *Commit) ?[:0]const u8 { 81 | if (internal.trace_log) log.debug("Commit.getSummary called", .{}); 82 | 83 | const ret = c.git_commit_summary(@ptrCast(self)); 84 | 85 | return if (ret) |c_str| std.mem.sliceTo(c_str, 0) else null; 86 | } 87 | 88 | /// Get the long "body" of the git commit message. 89 | /// 90 | /// The returned message is the body of the commit, comprising everything but the first paragraph of the message. Leading and 91 | /// trailing whitespaces are trimmed. 92 | pub fn getBody(self: *Commit) ?[:0]const u8 { 93 | if (internal.trace_log) log.debug("Commit.getBody called", .{}); 94 | 95 | const ret = c.git_commit_body(@ptrCast(self)); 96 | 97 | return if (ret) |c_str| std.mem.sliceTo(c_str, 0) else null; 98 | } 99 | 100 | /// Get the commit time (i.e. committer time) of a commit. 101 | pub fn getTime(self: *const Commit) i64 { 102 | if (internal.trace_log) log.debug("Commit.getTime called", .{}); 103 | 104 | return c.git_commit_time(@ptrCast(self)); 105 | } 106 | 107 | /// Get the commit timezone offset (i.e. committer's preferred timezone) of a commit. 108 | pub fn getTimeOffset(self: *const Commit) i32 { 109 | if (internal.trace_log) log.debug("Commit.getTimeOffset called", .{}); 110 | 111 | return c.git_commit_time_offset(@ptrCast(self)); 112 | } 113 | 114 | pub fn getCommitter(self: *const Commit) *const git.Signature { 115 | if (internal.trace_log) log.debug("Commit.getCommitter called", .{}); 116 | 117 | return @as( 118 | *const git.Signature, 119 | @ptrCast(c.git_commit_committer(@ptrCast(self))), 120 | ); 121 | } 122 | 123 | pub fn getAuthor(self: *const Commit) *const git.Signature { 124 | if (internal.trace_log) log.debug("Commit.getAuthor called", .{}); 125 | 126 | return @as( 127 | *const git.Signature, 128 | @ptrCast(c.git_commit_author(@ptrCast(self))), 129 | ); 130 | } 131 | 132 | pub fn committerWithMailmap(self: *const Commit, mail_map: ?*const git.Mailmap) !*git.Signature { 133 | if (internal.trace_log) log.debug("Commit.committerWithMailmap", .{}); 134 | 135 | var signature: *git.Signature = undefined; 136 | 137 | try internal.wrapCall("git_commit_committer_with_mailmap", .{ 138 | @as(*?*c.git_signature, @ptrCast(&signature)), 139 | @as(*const c.git_commit, @ptrCast(self)), 140 | @as(?*const c.git_mailmap, @ptrCast(mail_map)), 141 | }); 142 | 143 | return signature; 144 | } 145 | 146 | pub fn authorWithMailmap(self: *const Commit, mail_map: ?*const git.Mailmap) !*git.Signature { 147 | if (internal.trace_log) log.debug("Commit.authorWithMailmap called", .{}); 148 | 149 | var signature: *git.Signature = undefined; 150 | 151 | try internal.wrapCall("git_commit_author_with_mailmap", .{ 152 | @as(*?*c.git_signature, @ptrCast(&signature)), 153 | @as(*const c.git_commit, @ptrCast(self)), 154 | @as(?*const c.git_mailmap, @ptrCast(mail_map)), 155 | }); 156 | 157 | return signature; 158 | } 159 | 160 | pub fn getTree(self: *const Commit) !*git.Tree { 161 | if (internal.trace_log) log.debug("Commit.getTree called", .{}); 162 | 163 | var tree: *git.Tree = undefined; 164 | 165 | try internal.wrapCall("git_commit_tree", .{ 166 | @as(*?*c.git_tree, @ptrCast(&tree)), 167 | @as(*const c.git_commit, @ptrCast(self)), 168 | }); 169 | 170 | return tree; 171 | } 172 | 173 | pub fn getTreeId(self: *const Commit) !*const git.Oid { 174 | if (internal.trace_log) log.debug("Commit.getTreeId called", .{}); 175 | 176 | return @ptrCast(c.git_commit_tree_id(@ptrCast(self))); 177 | } 178 | 179 | pub fn getParentCount(self: *const Commit) u32 { 180 | if (internal.trace_log) log.debug("Commit.getParentCount called", .{}); 181 | 182 | return c.git_commit_parentcount(@ptrCast(self)); 183 | } 184 | 185 | pub fn getParent(self: *const Commit, parent_number: u32) !*Commit { 186 | if (internal.trace_log) log.debug("Commit.getParent called", .{}); 187 | 188 | var commit: *Commit = undefined; 189 | 190 | try internal.wrapCall("git_commit_parent", .{ 191 | @as(*?*c.git_commit, @ptrCast(&commit)), 192 | @as(*const c.git_commit, @ptrCast(self)), 193 | parent_number, 194 | }); 195 | 196 | return commit; 197 | } 198 | 199 | pub fn getParentId(self: *const Commit, parent_number: u32) ?*const git.Oid { 200 | if (internal.trace_log) log.debug("Commit.getParentId called", .{}); 201 | 202 | return @ptrCast(c.git_commit_parent_id( 203 | @ptrCast(self), 204 | parent_number, 205 | )); 206 | } 207 | 208 | pub fn getAncestor(self: *const Commit, ancestor_number: u32) !*Commit { 209 | if (internal.trace_log) log.debug("Commit.getAncestor called", .{}); 210 | 211 | var commit: *Commit = undefined; 212 | 213 | try internal.wrapCall("git_commit_nth_gen_ancestor", .{ 214 | @as(*?*c.git_commit, @ptrCast(&commit)), 215 | @as(*const c.git_commit, @ptrCast(self)), 216 | ancestor_number, 217 | }); 218 | 219 | return commit; 220 | } 221 | 222 | pub fn getHeaderField(self: *const Commit, field: [:0]const u8) !git.Buf { 223 | if (internal.trace_log) log.debug("Commit.getHeaderField called", .{}); 224 | 225 | var buf: git.Buf = .{}; 226 | 227 | try internal.wrapCall("git_commit_header_field", .{ 228 | @as(*c.git_buf, @ptrCast(&buf)), 229 | @as(*const c.git_commit, @ptrCast(self)), 230 | field.ptr, 231 | }); 232 | 233 | return buf; 234 | } 235 | 236 | pub fn amend( 237 | self: *const Commit, 238 | update_ref: ?[:0]const u8, 239 | author: ?*const git.Signature, 240 | committer: ?*const git.Signature, 241 | message_encoding: ?[:0]const u8, 242 | message: ?[:0]const u8, 243 | tree: ?*const git.Tree, 244 | ) !git.Oid { 245 | if (internal.trace_log) log.debug("Commit.amend called", .{}); 246 | 247 | var ret: git.Oid = undefined; 248 | 249 | const update_ref_temp = if (update_ref) |slice| slice.ptr else null; 250 | const encoding_temp = if (message_encoding) |slice| slice.ptr else null; 251 | const message_temp = if (message) |slice| slice.ptr else null; 252 | 253 | try internal.wrapCall("git_commit_amend", .{ 254 | @as(*c.git_oid, @ptrCast(&ret)), 255 | @as(*const c.git_commit, @ptrCast(self)), 256 | update_ref_temp, 257 | @as(?*const c.git_signature, @ptrCast(author)), 258 | @as(?*const c.git_signature, @ptrCast(committer)), 259 | encoding_temp, 260 | message_temp, 261 | @as(?*const c.git_tree, @ptrCast(tree)), 262 | }); 263 | 264 | return ret; 265 | } 266 | 267 | pub fn duplicate(self: *Commit) !*Commit { 268 | if (internal.trace_log) log.debug("Commit.duplicate called", .{}); 269 | 270 | var commit: *Commit = undefined; 271 | 272 | try internal.wrapCall("git_commit_dup", .{ 273 | @as(*?*c.git_commit, @ptrCast(&commit)), 274 | @as(*c.git_commit, @ptrCast(self)), 275 | }); 276 | 277 | return commit; 278 | } 279 | 280 | comptime { 281 | std.testing.refAllDecls(@This()); 282 | } 283 | }; 284 | 285 | /// Options for revert 286 | pub const RevertOptions = struct { 287 | /// For merge commits, the "mainline" is treated as the parent. 288 | mainline: bool = false, 289 | /// Options for the merging 290 | merge_options: git.MergeOptions = .{}, 291 | /// Options for the checkout 292 | checkout_options: git.CheckoutOptions = .{}, 293 | 294 | comptime { 295 | std.testing.refAllDecls(@This()); 296 | } 297 | }; 298 | 299 | comptime { 300 | std.testing.refAllDecls(@This()); 301 | } 302 | -------------------------------------------------------------------------------- /src/internal/make_c_option.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("c.zig"); 3 | 4 | const git = @import("../git.zig"); 5 | 6 | pub fn rebaseOptions(self: git.RebaseOptions) c.git_rebase_options { 7 | return .{ 8 | .version = c.GIT_REBASE_OPTIONS_VERSION, 9 | .quiet = @intFromBool(self.quiet), 10 | .inmemory = @intFromBool(self.in_memory), 11 | .rewrite_notes_ref = if (self.rewrite_notes_ref) |s| s.ptr else null, 12 | .merge_options = mergeOptions(self.merge_options), 13 | .checkout_options = checkoutOptions(self.checkout_options), 14 | .signing_cb = @ptrCast(self.signing_cb), 15 | .payload = self.payload, 16 | }; 17 | } 18 | 19 | pub fn diffOptions(self: git.DiffOptions) c.git_diff_options { 20 | return .{ 21 | .version = c.GIT_DIFF_OPTIONS_VERSION, 22 | .flags = @bitCast(self.flags), 23 | .ignore_submodules = @intFromEnum(self.ignore_submodules), 24 | .pathspec = @bitCast(self.pathspec), 25 | .notify_cb = @ptrCast(self.notify_cb), 26 | .progress_cb = @ptrCast(self.progress_cb), 27 | .payload = self.payload, 28 | .context_lines = self.context_lines, 29 | .interhunk_lines = self.interhunk_lines, 30 | .id_abbrev = self.id_abbrev, 31 | .max_size = self.max_size, 32 | .old_prefix = self.old_prefix, 33 | .new_prefix = self.new_prefix, 34 | }; 35 | } 36 | 37 | pub fn stashApplyOptions(self: git.StashApplyOptions) c.git_stash_apply_options { 38 | return .{ 39 | .version = c.GIT_STASH_APPLY_OPTIONS_VERSION, 40 | .flags = @bitCast(self.flags), 41 | .checkout_options = checkoutOptions(self.checkout_options), 42 | .progress_cb = @ptrCast(self.progress_callback), 43 | .progress_payload = self.progress_payload, 44 | }; 45 | } 46 | 47 | pub fn blobFilterOptions(self: git.BlobFilterOptions) c.git_blob_filter_options { 48 | return .{ 49 | .version = c.GIT_BLOB_FILTER_OPTIONS_VERSION, 50 | .flags = @bitCast(self.flags), 51 | .commit_id = @ptrCast(self.commit_id), 52 | }; 53 | } 54 | 55 | pub fn describeOptions(self: git.DescribeOptions) c.git_describe_options { 56 | return .{ 57 | .version = c.GIT_DESCRIBE_OPTIONS_VERSION, 58 | .max_candidates_tags = self.max_candidate_tags, 59 | .describe_strategy = @intFromEnum(self.describe_strategy), 60 | .pattern = if (self.pattern) |slice| slice.ptr else null, 61 | .only_follow_first_parent = @intFromBool(self.only_follow_first_parent), 62 | .show_commit_oid_as_fallback = @intFromBool(self.show_commit_oid_as_fallback), 63 | }; 64 | } 65 | 66 | pub fn describeFormatOptions(self: git.DescribeFormatOptions) c.git_describe_format_options { 67 | return .{ 68 | .version = c.GIT_DESCRIBE_FORMAT_OPTIONS_VERSION, 69 | .abbreviated_size = self.abbreviated_size, 70 | .always_use_long_format = @intFromBool(self.always_use_long_format), 71 | .dirty_suffix = if (self.dirty_suffix) |slice| slice.ptr else null, 72 | }; 73 | } 74 | 75 | pub fn filterOptions(self: git.FilterOptions) c.git_filter_options { 76 | return .{ 77 | .version = c.GIT_FILTER_OPTIONS_VERSION, 78 | .flags = @bitCast(self.flags), 79 | .commit_id = @ptrCast(self.commit_id), 80 | }; 81 | } 82 | 83 | pub fn repositoryInitOptions(self: git.RepositoryInitOptions) c.git_repository_init_options { 84 | return .{ 85 | .version = c.GIT_REPOSITORY_INIT_OPTIONS_VERSION, 86 | .flags = self.flags.toInt(), 87 | .mode = self.mode.toInt(), 88 | .workdir_path = if (self.workdir_path) |slice| slice.ptr else null, 89 | .description = if (self.description) |slice| slice.ptr else null, 90 | .template_path = if (self.template_path) |slice| slice.ptr else null, 91 | .initial_head = if (self.initial_head) |slice| slice.ptr else null, 92 | .origin_url = if (self.origin_url) |slice| slice.ptr else null, 93 | }; 94 | } 95 | 96 | pub fn hashsigOptions(self: git.HashsigOptions) c.git_hashsig_option_t { 97 | var ret: c.git_hashsig_option_t = 0; 98 | 99 | if (self.allow_small_files) { 100 | ret |= c.GIT_HASHSIG_ALLOW_SMALL_FILES; 101 | } 102 | 103 | switch (self.whitespace_mode) { 104 | .normal => ret |= c.GIT_HASHSIG_NORMAL, 105 | .ignore_whitespace => ret |= c.GIT_HASHSIG_IGNORE_WHITESPACE, 106 | .smart_whitespace => ret |= c.GIT_HASHSIG_SMART_WHITESPACE, 107 | } 108 | 109 | return ret; 110 | } 111 | 112 | pub fn mergeOptions(self: git.MergeOptions) c.git_merge_options { 113 | return .{ 114 | .version = c.GIT_MERGE_OPTIONS_VERSION, 115 | .flags = @bitCast(self.flags), 116 | .rename_threshold = self.rename_threshold, 117 | .target_limit = self.target_limit, 118 | .metric = @ptrCast(self.metric), 119 | .recursion_limit = self.recursion_limit, 120 | .default_driver = if (self.default_driver) |ptr| ptr.ptr else null, 121 | .file_favor = @intFromEnum(self.file_favor), 122 | .file_flags = @bitCast(self.file_flags), 123 | }; 124 | } 125 | 126 | pub fn fileStatusOptions(self: git.FileStatusOptions) c.git_status_options { 127 | return .{ 128 | .version = c.GIT_STATUS_OPTIONS_VERSION, 129 | .show = @intFromEnum(self.show), 130 | .flags = @as(c_uint, @bitCast(self.flags)), 131 | .pathspec = @bitCast(self.pathspec), 132 | .baseline = @ptrCast(self.baseline), 133 | }; 134 | } 135 | 136 | pub fn applyOptions(self: git.ApplyOptions) c.git_apply_options { 137 | return .{ 138 | .version = c.GIT_APPLY_OPTIONS_VERSION, 139 | .delta_cb = @ptrCast(self.delta_cb), 140 | .hunk_cb = @ptrCast(self.hunk_cb), 141 | .payload = null, 142 | .flags = @bitCast(self.flags), 143 | }; 144 | } 145 | 146 | pub fn applyOptionsWithUserData(comptime T: type, self: git.ApplyOptionsWithUserData(T)) c.git_apply_options { 147 | return .{ 148 | .version = c.GIT_APPLY_OPTIONS_VERSION, 149 | .delta_cb = @ptrCast(self.delta_cb), 150 | .hunk_cb = @ptrCast(self.hunk_cb), 151 | .payload = self.payload, 152 | .flags = @bitCast(self.flags), 153 | }; 154 | } 155 | 156 | comptime { 157 | if (@import("builtin").is_test) { 158 | var temp: u8 = undefined; 159 | _ = applyOptionsWithUserData(*u8, .{ .payload = &temp }); 160 | } 161 | } 162 | 163 | pub fn blameOptions(self: git.BlameOptions) c.git_blame_options { 164 | return .{ 165 | .version = c.GIT_BLAME_OPTIONS_VERSION, 166 | .flags = @bitCast(self.flags), 167 | .min_match_characters = self.min_match_characters, 168 | .newest_commit = @bitCast(self.newest_commit), 169 | .oldest_commit = @bitCast(self.oldest_commit), 170 | .min_line = self.min_line, 171 | .max_line = self.max_line, 172 | }; 173 | } 174 | 175 | pub fn checkoutOptions(self: git.CheckoutOptions) c.git_checkout_options { 176 | return .{ 177 | .version = c.GIT_CHECKOUT_OPTIONS_VERSION, 178 | .checkout_strategy = @bitCast(self.checkout_strategy), 179 | .disable_filters = @intFromBool(self.disable_filters), 180 | .dir_mode = self.dir_mode, 181 | .file_mode = self.file_mode, 182 | .file_open_flags = self.file_open_flags, 183 | .notify_flags = @bitCast(self.notify_flags), 184 | .notify_cb = @ptrCast(self.notify_cb), 185 | .notify_payload = self.notify_payload, 186 | .progress_cb = @ptrCast(self.progress_cb), 187 | .progress_payload = self.progress_payload, 188 | .paths = @bitCast(self.paths), 189 | .baseline = @ptrCast(self.baseline), 190 | .baseline_index = @ptrCast(self.baseline_index), 191 | .target_directory = if (self.target_directory) |ptr| ptr.ptr else null, 192 | .ancestor_label = if (self.ancestor_label) |ptr| ptr.ptr else null, 193 | .our_label = if (self.our_label) |ptr| ptr.ptr else null, 194 | .their_label = if (self.their_label) |ptr| ptr.ptr else null, 195 | .perfdata_cb = @ptrCast(self.perfdata_cb), 196 | .perfdata_payload = self.perfdata_payload, 197 | }; 198 | } 199 | 200 | pub fn cherrypickOptions(self: git.CherrypickOptions) c.git_cherrypick_options { 201 | return .{ 202 | .version = c.GIT_CHERRYPICK_OPTIONS_VERSION, 203 | .mainline = @intFromBool(self.mainline), 204 | .merge_opts = mergeOptions(self.merge_options), 205 | .checkout_opts = checkoutOptions(self.checkout_options), 206 | }; 207 | } 208 | 209 | pub fn attributeOptions(self: git.AttributeOptions) c.git_attr_options { 210 | return .{ 211 | .version = c.GIT_ATTR_OPTIONS_VERSION, 212 | .flags = attributeFlags(self.flags), 213 | .commit_id = @ptrCast(self.commit_id), 214 | }; 215 | } 216 | 217 | pub fn attributeFlags(self: git.AttributeFlags) c_uint { 218 | var result: c_uint = 0; 219 | 220 | switch (self.location) { 221 | .file_then_index => {}, 222 | .index_then_file => result |= c.GIT_ATTR_CHECK_INDEX_THEN_FILE, 223 | .index_only => result |= c.GIT_ATTR_CHECK_INDEX_ONLY, 224 | } 225 | 226 | if (self.extended.no_system) { 227 | result |= c.GIT_ATTR_CHECK_NO_SYSTEM; 228 | } 229 | 230 | if (self.extended.include_head) { 231 | result |= c.GIT_ATTR_CHECK_INCLUDE_HEAD; 232 | } 233 | 234 | if (self.extended.include_commit) { 235 | result |= c.GIT_ATTR_CHECK_INCLUDE_COMMIT; 236 | } 237 | 238 | return result; 239 | } 240 | 241 | pub fn worktreeAddOptions(self: git.WorktreeAddOptions) c.git_worktree_add_options { 242 | return .{ 243 | .version = c.GIT_WORKTREE_ADD_OPTIONS_VERSION, 244 | .lock = @intFromBool(self.lock), 245 | .ref = @ptrCast(self.ref), 246 | }; 247 | } 248 | 249 | pub fn revertOptions(self: git.RevertOptions) c.git_revert_options { 250 | return .{ 251 | .version = c.GIT_REVERT_OPTIONS_VERSION, 252 | .mainline = @intFromBool(self.mainline), 253 | .merge_opts = mergeOptions(self.merge_options), 254 | .checkout_opts = checkoutOptions(self.checkout_options), 255 | }; 256 | } 257 | 258 | pub fn fetchOptions(self: git.FetchOptions) c.git_fetch_options { 259 | return .{ 260 | .version = c.GIT_FETCH_OPTIONS_VERSION, 261 | .callbacks = @bitCast(self.callbacks), 262 | .prune = @intFromEnum(self.prune), 263 | .update_fetchhead = @intFromBool(self.update_fetchhead), 264 | .download_tags = @intFromEnum(self.download_tags), 265 | .proxy_opts = proxyOptions(self.proxy_opts), 266 | .custom_headers = @bitCast(self.custom_headers), 267 | }; 268 | } 269 | 270 | pub fn cloneOptions(self: git.CloneOptions) c.git_clone_options { 271 | return .{ 272 | .version = c.GIT_CHECKOUT_OPTIONS_VERSION, 273 | .checkout_opts = checkoutOptions(self.checkout_options), 274 | .fetch_opts = fetchOptions(self.fetch_options), 275 | .bare = @intFromBool(self.bare), 276 | .local = @intFromEnum(self.local), 277 | .checkout_branch = if (self.checkout_branch) |b| b.ptr else null, 278 | .repository_cb = @ptrCast(self.repository_cb), 279 | .repository_cb_payload = self.repository_cb_payload, 280 | .remote_cb = @ptrCast(self.remote_cb), 281 | .remote_cb_payload = self.remote_cb_payload, 282 | }; 283 | } 284 | 285 | pub fn proxyOptions(self: git.ProxyOptions) c.git_proxy_options { 286 | return .{ 287 | .version = c.GIT_PROXY_OPTIONS_VERSION, 288 | .type = @intFromEnum(self.proxy_type), 289 | .url = if (self.url) |s| s.ptr else null, 290 | .credentials = @ptrCast(self.credentials), 291 | .payload = self.payload, 292 | .certificate_check = @ptrCast(self.certificate_check), 293 | }; 294 | } 295 | 296 | pub fn createOptions(self: git.RemoteCreateOptions) c.git_remote_create_options { 297 | return .{ 298 | .version = c.GIT_STATUS_OPTIONS_VERSION, 299 | .repository = @ptrCast(self.repository), 300 | .name = if (self.name) |ptr| ptr.ptr else null, 301 | .fetchspec = if (self.fetchspec) |ptr| ptr.ptr else null, 302 | .flags = @bitCast(self.flags), 303 | }; 304 | } 305 | 306 | pub fn pushOptions(self: git.PushOptions) c.git_push_options { 307 | return .{ 308 | .version = c.GIT_PUSH_OPTIONS_VERSION, 309 | .pb_parallelism = self.pb_parallelism, 310 | .callbacks = @bitCast(self.callbacks), 311 | .proxy_opts = proxyOptions(self.proxy_opts), 312 | .custom_headers = @bitCast(self.custom_headers), 313 | }; 314 | } 315 | 316 | pub fn pruneOptions(self: git.PruneOptions) c.git_worktree_prune_options { 317 | return .{ 318 | .version = c.GIT_WORKTREE_PRUNE_OPTIONS_VERSION, 319 | .flags = @bitCast(self), 320 | }; 321 | } 322 | 323 | comptime { 324 | std.testing.refAllDecls(@This()); 325 | } 326 | -------------------------------------------------------------------------------- /src/rebase.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("internal/c.zig"); 3 | const internal = @import("internal/internal.zig"); 4 | const log = std.log.scoped(.git); 5 | 6 | const git = @import("git.zig"); 7 | 8 | pub const Rebase = opaque { 9 | pub fn deinit(self: *Rebase) void { 10 | if (internal.trace_log) log.debug("Rebase.deinit called", .{}); 11 | 12 | c.git_rebase_free(@as(*c.git_rebase, @ptrCast(self))); 13 | } 14 | 15 | /// Gets the original `HEAD` ref name for merge rebases. 16 | pub fn originalHeadName(self: *Rebase) [:0]const u8 { 17 | if (internal.trace_log) log.debug("Rebase.originalHeadName called", .{}); 18 | 19 | return std.mem.sliceTo(c.git_rebase_orig_head_name(@as(*c.git_rebase, @ptrCast(self))), 0); 20 | } 21 | 22 | /// Gets the original `HEAD` id for merge rebases. 23 | pub fn originalHeadId(self: *Rebase) *const git.Oid { 24 | if (internal.trace_log) log.debug("Rebase.originalHeadId called", .{}); 25 | 26 | return @as( 27 | *const git.Oid, 28 | @ptrCast(c.git_rebase_orig_head_id(@as(*c.git_rebase, @ptrCast(self)))), 29 | ); 30 | } 31 | 32 | /// Gets the `onto` ref name for merge rebases. 33 | pub fn originalOntoName(self: *Rebase) [:0]const u8 { 34 | if (internal.trace_log) log.debug("Rebase.originalOntoName called", .{}); 35 | 36 | return std.mem.sliceTo(c.git_rebase_onto_name(@as(*c.git_rebase, @ptrCast(self))), 0); 37 | } 38 | 39 | /// Gets the `onto` id for merge rebases. 40 | pub fn originalOntoId(self: *Rebase) *const git.Oid { 41 | if (internal.trace_log) log.debug("Rebase.originalOntoId called", .{}); 42 | 43 | return @as( 44 | *const git.Oid, 45 | @ptrCast(c.git_rebase_onto_id(@as(*c.git_rebase, @ptrCast(self)))), 46 | ); 47 | } 48 | 49 | /// Gets the count of rebase operations that are to be applied. 50 | pub fn operationEntryCount(self: *Rebase) usize { 51 | if (internal.trace_log) log.debug("Rebase.operationEntryCount called", .{}); 52 | 53 | return c.git_rebase_operation_entrycount(@as(*c.git_rebase, @ptrCast(self))); 54 | } 55 | 56 | /// Gets the index of the rebase operation that is currently being applied. If the first operation has not yet been applied 57 | /// (because you have called `init` but not yet `next`) then this returns `null`. 58 | pub fn currentOperation(self: *Rebase) ?usize { 59 | if (internal.trace_log) log.debug("Rebase.currentOperation called", .{}); 60 | 61 | const ret = c.git_rebase_operation_current(@as(*c.git_rebase, @ptrCast(self))); 62 | 63 | if (ret == c.GIT_REBASE_NO_OPERATION) return null; 64 | 65 | return ret; 66 | } 67 | 68 | /// Gets the rebase operation specified by the given index. 69 | /// 70 | /// ## Parameters 71 | /// * `index` - The index of the rebase operation to retrieve. 72 | pub fn getOperation(self: *Rebase, index: usize) ?*RebaseOperation { 73 | if (internal.trace_log) log.debug("Rebase.getOperation called", .{}); 74 | 75 | return @as( 76 | ?*RebaseOperation, 77 | @ptrCast(c.git_rebase_operation_byindex( 78 | @as(*c.git_rebase, @ptrCast(self)), 79 | index, 80 | )), 81 | ); 82 | } 83 | 84 | /// Performs the next rebase operation and returns the information about it. If the operation is one that applies a patch 85 | /// (which is any operation except `OperationType.exec`) then the patch will be applied and the index and working directory 86 | /// will be updated with the changes. If there are conflicts, you will need to address those before committing the changes. 87 | pub fn next(self: *Rebase) !*RebaseOperation { 88 | if (internal.trace_log) log.debug("Rebase.next called", .{}); 89 | 90 | var ret: *RebaseOperation = undefined; 91 | 92 | try internal.wrapCall("git_rebase_next", .{ 93 | @as(*?*c.git_rebase_operation, @ptrCast(&ret)), 94 | @as(*c.git_rebase, @ptrCast(self)), 95 | }); 96 | 97 | return ret; 98 | } 99 | 100 | /// Gets the index produced by the last operation, which is the result of `Rebase.next` and which will be committed by the 101 | /// next invocation of `Rebase.commit`. This is useful for resolving conflicts in an in-memory rebase before committing them. 102 | /// You must call `Index.deinit` when you are finished with this. 103 | /// 104 | /// This is only applicable for in-memory rebases; for rebases within a working directory, the changes were applied to the 105 | /// repository's index. 106 | pub fn inMemoryIndex(self: *Rebase) !*git.Index { 107 | if (internal.trace_log) log.debug("Rebase.inMemoryIndex called", .{}); 108 | 109 | var ret: *git.Index = undefined; 110 | 111 | try internal.wrapCall("git_rebase_inmemory_index", .{ 112 | @as(*?*c.git_index, @ptrCast(&ret)), 113 | @as(*c.git_rebase, @ptrCast(self)), 114 | }); 115 | 116 | return ret; 117 | } 118 | 119 | /// Commits the current patch. You must have resolved any conflicts that were introduced during the patch application from the 120 | /// `Rebase.next` invocation. 121 | /// 122 | /// ## Parameters 123 | /// * `author` - The author of the updated commit, or `null` to keep the author from the original commit. 124 | /// * `committer` - The committer of the rebase. 125 | /// * `message_encoding` - The encoding for the message in the commit, represented with a standard encoding name. If message 126 | /// is `null`, this should also be `null`, and the encoding from the original commit will be maintained. 127 | /// If message is specified, this may be `null` to indicate that "UTF-8" is to be used. 128 | /// * `message` - he message for this commit, or NULL to use the message from the original commit. 129 | pub fn commit( 130 | self: *Rebase, 131 | author: ?*const git.Signature, 132 | committer: *const git.Signature, 133 | message_encoding: ?[:0]const u8, 134 | message: ?[:0]const u8, 135 | ) !git.Oid { 136 | if (internal.trace_log) log.debug("Rebase.commit called", .{}); 137 | 138 | var ret: git.Oid = undefined; 139 | 140 | const c_message_encoding = if (message_encoding) |s| s.ptr else null; 141 | const c_message = if (message) |s| s.ptr else null; 142 | 143 | try internal.wrapCall("git_rebase_commit", .{ 144 | @as(*c.git_oid, @ptrCast(&ret)), 145 | @as(*c.git_rebase, @ptrCast(self)), 146 | @as(?*const c.git_signature, @ptrCast(author)), 147 | @as(*const c.git_signature, @ptrCast(committer)), 148 | c_message_encoding, 149 | c_message, 150 | }); 151 | 152 | return ret; 153 | } 154 | 155 | /// Aborts a rebase that is currently in progress, resetting the repository and working directory to their state before rebase 156 | /// began. 157 | pub fn abort(self: *Rebase) !void { 158 | if (internal.trace_log) log.debug("Rebase.abort called", .{}); 159 | 160 | try internal.wrapCall("git_rebase_abort", .{ 161 | @as(*c.git_rebase, @ptrCast(self)), 162 | }); 163 | } 164 | 165 | /// Finishes a rebase that is currently in progress once all patches have been applied. 166 | pub fn finish(self: *Rebase, signature: ?*const git.Signature) !void { 167 | if (internal.trace_log) log.debug("Rebase.finish called", .{}); 168 | 169 | try internal.wrapCall("git_rebase_finish", .{ 170 | @as(*c.git_rebase, @ptrCast(self)), 171 | @as(?*const c.git_signature, @ptrCast(signature)), 172 | }); 173 | } 174 | 175 | comptime { 176 | std.testing.refAllDecls(@This()); 177 | } 178 | }; 179 | 180 | /// A rebase operation 181 | /// 182 | /// Describes a single instruction/operation to be performed during the rebase. 183 | pub const RebaseOperation = extern struct { 184 | /// The type of rebase operation. 185 | operation_type: OperationType, 186 | 187 | /// The commit ID being cherry-picked. This will be populated for all operations except those of type `OperationType.exec`. 188 | id: git.Oid, 189 | 190 | /// The executable the user has requested be run. This will only be populated for operations of type `OperationType.exec`. 191 | exec: [*:0]const u8, 192 | 193 | /// Type of rebase operation in-progress after calling `Rebase.next`. 194 | pub const OperationType = enum(c_uint) { 195 | /// The given commit is to be cherry-picked. The client should commit the changes and continue if there are no conflicts. 196 | pick = 0, 197 | 198 | /// The given commit is to be cherry-picked, but the client should prompt the user to provide an updated commit message. 199 | reword, 200 | 201 | /// The given commit is to be cherry-picked, but the client should stop to allow the user to edit the changes before 202 | /// committing them. 203 | edit, 204 | 205 | /// The given commit is to be squashed into the previous commit. The commit message will be merged with the previous 206 | /// message. 207 | squash, 208 | 209 | /// The given commit is to be squashed into the previous commit. The commit message from this commit will be discarded. 210 | fixup, 211 | 212 | /// No commit will be cherry-picked. The client should run the given command and (if successful) continue. 213 | exec, 214 | }; 215 | 216 | test { 217 | try std.testing.expectEqual(@sizeOf(c.git_rebase_operation), @sizeOf(RebaseOperation)); 218 | try std.testing.expectEqual(@bitSizeOf(c.git_rebase_operation), @bitSizeOf(RebaseOperation)); 219 | } 220 | 221 | comptime { 222 | std.testing.refAllDecls(@This()); 223 | } 224 | }; 225 | 226 | /// Use to tell the rebase machinery how to operate. 227 | pub const RebaseOptions = struct { 228 | /// Used by `Repository.rebaseInit`, this will instruct other clients working on this rebase that you want a quiet rebase 229 | /// experience, which they may choose to provide in an application-specific manner. This has no effect upon libgit2 directly, 230 | /// but is provided for interoperability between Git tools. 231 | quiet: bool = false, 232 | 233 | /// Used by `Repository.rebaseInit`, this will begin an in-memory rebase, which will allow callers to step through the rebase 234 | /// operations andcommit the rebased changes, but will not rewind HEAD or update the repository to be in a rebasing state. 235 | /// This will not interfere with the working directory (if there is one). 236 | in_memory: bool = false, 237 | 238 | /// Used by `Rebase.finish`, this is the name of the notes reference used to rewrite notes for rebased commits when finishing 239 | /// the rebase; if `null`, the contents of the configuration option `notes.rewriteRef` is examined, unless the configuration 240 | /// option `notes.rewrite.rebase` is set to false. If `notes.rewriteRef` is also `null`, notes will not be rewritten. 241 | rewrite_notes_ref: ?[:0]const u8 = null, 242 | 243 | /// Options to control how trees are merged during `Rebase.next`. 244 | merge_options: git.MergeOptions = .{}, 245 | 246 | /// Options to control how files are written during `Repository.rebaseInit`, `Rebase.next` and `Rebase.abort`. Note that a 247 | /// minimum strategy of `safe` is defaulted in `init` and `next`, and a minimum strategy of `force` is defaulted in `abort` to 248 | /// match git semantics. 249 | checkout_options: git.CheckoutOptions = .{}, 250 | 251 | /// If provided, this will be called with the commit content, allowing a signature to be added to the rebase commit. Can be 252 | /// skipped with GitError.Passthrough. If `errorToCInt(GitError.Passthrough)` is returned, a commit will be made without a 253 | /// signature. This field is only used when performing `Rebase.commit`. 254 | /// 255 | /// The callback will be called with the commit content, giving a user an opportunity to sign the commit content. 256 | /// The signature_field buf may be left empty to specify the default field "gpgsig". 257 | /// 258 | /// Signatures can take the form of any string, and can be created on an arbitrary header field. Signatures are most commonly 259 | /// used for verifying authorship of a commit using GPG or a similar cryptographically secure signing algorithm. 260 | /// See https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work for more details. 261 | /// 262 | /// When the callback: 263 | /// - returns `errorToCInt(GitError.Passthrough)`, no signature will be added to the commit. 264 | /// - returns < 0, commit creation will be aborted. 265 | /// - returns 0, the signature parameter is expected to be filled. 266 | signing_cb: ?*const fn ( 267 | signature: *git.Buf, 268 | signature_field: *git.Buf, 269 | commit_content: [*:0]const u8, 270 | payload: ?*anyopaque, 271 | ) callconv(.C) c_int = null, 272 | 273 | /// This will be passed to each of the callbacks in this struct as the last parameter. 274 | payload: ?*anyopaque = null, 275 | 276 | comptime { 277 | std.testing.refAllDecls(@This()); 278 | } 279 | }; 280 | 281 | comptime { 282 | std.testing.refAllDecls(@This()); 283 | } 284 | --------------------------------------------------------------------------------