├── .gitignore ├── LICENSE ├── README.md ├── build.zig └── src ├── commit.zig ├── helpers.zig ├── index.zig ├── init.zig ├── main.zig ├── object.zig ├── pack.zig ├── pack_index.zig ├── ref.zig ├── status.zig ├── tag.zig └── tree.zig /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dante Catalfamo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-git 2 | 3 | Implementing git structures and functions in pure zig. 4 | 5 | Still very much a work in progress. Some commands are redundant and some will get replaced. 6 | 7 | This project is primarily for learning purposes and not meant to be a replacement for something like libgit2. 8 | 9 | It doesn't support pack deltas yet. 10 | 11 | ## Command 12 | 13 | The `zig-git` command currently supports the following subcommands: 14 | * `add ` - Add a file or directory to the git index 15 | * `branch` - List current branches (`refs/heads/`) in a format similar to `git branch` 16 | * `branch-create ` - Create a new branch from the current branch 17 | * `checkout` - Checkout a ref or commit 18 | * `commit` - Commit the current index (as a test, doesn't use correct information) 19 | * `index` - List out the content of the index 20 | * `init [directory]` - Create a new git repository 21 | * `log` - List commits 22 | * `read-commit ` - Parse and display a commit 23 | * `read-object ` - Dump the decompressed contents of an object to stdout 24 | * `read-pack ` - Parse a pack file and iterate over its contents 25 | * `read-pack-index ` - Search a pack file index for the offset of an object with a hash in a pack file 26 | * `read-ref [ref]` - Display a ref and what it points to, or all refs if no argument is given 27 | * `read-tag [ref]` - Parse and display a tag 28 | * `read-tree ` - Parse and display the all files in a tree 29 | * `refs` - List all refs 30 | * `rm ` - Remove file from index 31 | * `root` - Print the root of the current git project 32 | * `status` - Branch status 33 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | // declared module for use as library 8 | _ = b.addModule("zig-git", .{ 9 | .source_file = .{ .path = "src/main.zig" }, 10 | }); 11 | 12 | const exe = b.addExecutable(.{ 13 | .name = "zig-git", 14 | .root_source_file = .{ .path = "src/main.zig" }, 15 | .target = target, 16 | .optimize = optimize, 17 | }); 18 | b.installArtifact(exe); 19 | 20 | const run_cmd = b.addRunArtifact(exe); 21 | run_cmd.step.dependOn(b.getInstallStep()); 22 | if (b.args) |args| { 23 | run_cmd.addArgs(args); 24 | } 25 | 26 | const run_step = b.step("run", "Run the app"); 27 | run_step.dependOn(&run_cmd.step); 28 | 29 | const exe_tests = b.addTest(.{ 30 | .root_source_file = .{ .path = "src/main.zig" }, 31 | .target = target, 32 | .optimize = optimize, 33 | }); 34 | 35 | const test_step = b.step("test", "Run unit tests"); 36 | test_step.dependOn(&b.addRunArtifact(exe_tests).step); 37 | } 38 | -------------------------------------------------------------------------------- /src/commit.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const object_zig = @import("object.zig"); 9 | const loadObject = object_zig.loadObject; 10 | const saveObject = object_zig.saveObject; 11 | 12 | const tree_zig = @import("tree.zig"); 13 | 14 | const index_zig = @import("index.zig"); 15 | const Index = index_zig.Index; 16 | 17 | const helpers = @import("helpers.zig"); 18 | const ObjectNameList = helpers.ObjectNameList; 19 | const hexDigestToObjectName = helpers.hexDigestToObjectName; 20 | 21 | /// Restore all of a commit's files in a repo 22 | pub fn restoreCommit(allocator: mem.Allocator, repo_path: []const u8, commit_object_name: [20]u8) !*Index { 23 | const git_dir_path = try helpers.repoToGitDir(allocator, repo_path); 24 | defer allocator.free(git_dir_path); 25 | 26 | const commit = try readCommit(allocator, git_dir_path, commit_object_name); 27 | defer commit.deinit(); 28 | 29 | return try tree_zig.restoreTree(allocator, repo_path, commit.tree); 30 | } 31 | 32 | /// Writes a commit object and returns its name 33 | pub fn writeCommit(allocator: mem.Allocator, git_dir_path: []const u8, commit: Commit) ![20]u8 { 34 | var commit_data = std.ArrayList(u8).init(allocator); 35 | defer commit_data.deinit(); 36 | 37 | const writer = commit_data.writer(); 38 | 39 | try writer.print("tree {s}\n", .{ std.fmt.fmtSliceHexLower(&commit.tree) }); 40 | for (commit.parents.items) |parent| { 41 | try writer.print("parent {s}\n", .{ std.fmt.fmtSliceHexLower(&parent) }); 42 | } 43 | const author = commit.author; 44 | try writer.print("author {}\n", .{ author }); 45 | const committer = commit.committer; 46 | try writer.print("committer {}\n", .{ committer }); 47 | 48 | try writer.print("\n{s}\n", .{ commit.message }); 49 | 50 | return saveObject(allocator, git_dir_path, commit_data.items, .commit); 51 | } 52 | 53 | /// Returns a Commit with a certain object name 54 | pub fn readCommit(allocator: mem.Allocator, git_dir_path: []const u8, commit_object_name: [20]u8) !*Commit { 55 | var commit_data = std.ArrayList(u8).init(allocator); 56 | defer commit_data.deinit(); 57 | 58 | const commit_object_header = try loadObject(allocator, git_dir_path, commit_object_name, commit_data.writer()); 59 | if (commit_object_header.type != .commit) { 60 | return error.InvalidObjectType; 61 | } 62 | 63 | var tree: ?[20]u8 = null; 64 | var parents = ObjectNameList.init(allocator); 65 | var author: ?Commit.Committer = null; 66 | var committer: ?Commit.Committer = null; 67 | var pgp_signature: ?std.ArrayList(u8) = null; 68 | 69 | errdefer { 70 | parents.deinit(); 71 | if (author) |valid_author| { 72 | valid_author.deinit(allocator); 73 | } 74 | if (committer) |valid_committer| { 75 | valid_committer.deinit(allocator); 76 | } 77 | if (pgp_signature) |sig| { 78 | sig.deinit(); 79 | } 80 | } 81 | 82 | var lines = mem.split(u8, commit_data.items, "\n"); 83 | var in_pgp = false; 84 | 85 | while (lines.next()) |line| { 86 | if (mem.eql(u8, line, "")) { 87 | break; 88 | } 89 | if (in_pgp and mem.indexOf(u8, line, "-----END PGP SIGNATURE-----") != null) { 90 | try pgp_signature.?.appendSlice(line); 91 | in_pgp = false; 92 | // PGP signatures seem to have a trailing line with one space 93 | const trailing_line = lines.next() orelse return error.MissingTrailingPGPLine; 94 | if (!mem.eql(u8, trailing_line, " ")) { 95 | std.debug.print("Trailing PGP line: \"{s}\"\n", .{ trailing_line }); 96 | return error.TrailingPGPLineNotEmpty; 97 | } 98 | continue; 99 | } 100 | if (in_pgp) { 101 | try pgp_signature.?.appendSlice(line); 102 | try pgp_signature.?.append('\n'); 103 | continue; 104 | } 105 | var words = mem.tokenize(u8, line, " "); 106 | const key = words.next() orelse { 107 | std.debug.print("commit data:\n{s}\n", .{ commit_data.items }); 108 | return error.MissingCommitPropertyKey; 109 | }; 110 | if (mem.eql(u8, key, "tree")) { 111 | const hex = words.next() orelse return error.InvalidTreeObjectName; 112 | tree = try hexDigestToObjectName(hex); 113 | } else if (mem.eql(u8, key, "parent")) { 114 | const hex = words.next() orelse return error.InvalidParentObjectName; 115 | try parents.append(try hexDigestToObjectName(hex)); 116 | } else if (mem.eql(u8, key, "author")) { 117 | author = try Commit.Committer.parse(allocator, words.rest()); 118 | } else if (mem.eql(u8, key, "committer")) { 119 | committer = try Commit.Committer.parse(allocator, words.rest()); 120 | } else if (mem.eql(u8, key, "gpgsig")) { 121 | in_pgp = true; 122 | pgp_signature = std.ArrayList(u8).init(allocator); 123 | try pgp_signature.?.appendSlice(words.rest()); 124 | } 125 | } 126 | 127 | const message = try allocator.dupe(u8, lines.rest()); 128 | errdefer allocator.free(message); 129 | const commit = try allocator.create(Commit); 130 | errdefer allocator.destroy(commit); 131 | 132 | commit.* = Commit{ 133 | .allocator = allocator, 134 | .tree = tree orelse return error.MissingTree, 135 | .parents = parents, 136 | .author = author orelse return error.MissingAuthor, 137 | .committer = committer orelse return error.MissingCommitter, 138 | .message = message, 139 | .pgp_signature = if (pgp_signature) |*sig| try sig.toOwnedSlice() else null, 140 | }; 141 | 142 | return commit; 143 | } 144 | 145 | pub const Commit = struct { 146 | allocator: mem.Allocator, 147 | tree: [20]u8, 148 | parents: ObjectNameList, 149 | author: Committer, 150 | committer: Committer, 151 | message: []const u8, 152 | pgp_signature: ?[]const u8 = null, 153 | 154 | pub fn deinit(self: *const Commit) void { 155 | self.parents.deinit(); 156 | self.author.deinit(self.allocator); 157 | self.committer.deinit(self.allocator); 158 | self.allocator.free(self.message); 159 | if (self.pgp_signature) |sig| { 160 | self.allocator.free(sig); 161 | } 162 | self.allocator.destroy(self); 163 | } 164 | 165 | pub fn format(self: Commit, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 166 | _ = fmt; 167 | _ = options; 168 | 169 | try out_stream.print("Commit{{\n", .{}); 170 | try out_stream.print(" Tree: {s}\n", .{ std.fmt.fmtSliceHexLower(&self.tree) }); 171 | for (self.parents.items) |parent| { 172 | try out_stream.print(" Parent: {s}\n", .{ std.fmt.fmtSliceHexLower(&parent) }); 173 | } 174 | try out_stream.print(" Author: {}\n", .{ self.author }); 175 | try out_stream.print(" Committer: {}\n", .{ self.committer }); 176 | try out_stream.print(" Message: {s}", .{ self.message }); 177 | if (self.message[self.message.len-1] != '\n') { 178 | try out_stream.print("\n", .{}); 179 | } 180 | if (self.pgp_signature) |sig| { 181 | try out_stream.print(" PGP Signature:\n{s}\n", .{ sig }); 182 | } 183 | try out_stream.print("}}\n", .{}); 184 | } 185 | 186 | pub const Committer = struct { 187 | name: []const u8, 188 | email: []const u8, 189 | time: i64, 190 | timezone: i16, 191 | 192 | pub fn deinit(self: Committer, allocator: mem.Allocator) void { 193 | allocator.free(self.name); 194 | allocator.free(self.email); 195 | } 196 | 197 | pub fn parse(allocator: mem.Allocator, line: []const u8) !Committer { 198 | var email_split = mem.tokenize(u8, line, "<>"); 199 | const name = mem.trimRight(u8, email_split.next() orelse return error.InvalidCommitter, " "); 200 | const email = email_split.next() orelse return error.InvalidCommitter; 201 | var time_split = mem.tokenize(u8, email_split.rest(), " "); 202 | const unix_time = time_split.next() orelse return error.InvalidCommitter; 203 | const timezone = time_split.next() orelse return error.InvalidCommitter; 204 | 205 | return .{ 206 | .name = try allocator.dupe(u8, name), 207 | .email = try allocator.dupe(u8, email), 208 | .time = try std.fmt.parseInt(i64, unix_time, 10), 209 | .timezone = try std.fmt.parseInt(i16, timezone, 10), 210 | }; 211 | } 212 | 213 | pub fn format(self: Committer, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 214 | _ = fmt; 215 | _ = options; 216 | const sign: u8 = if (self.timezone > 0) '+' else '-'; 217 | const timezone: u16 = @intCast(@abs(self.timezone)); 218 | try out_stream.print("{s} <{s}> {d} {c}{d:0>4}", .{ self.name, self.email, self.time, sign, timezone }); 219 | } 220 | }; 221 | }; 222 | -------------------------------------------------------------------------------- /src/helpers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const Tree = @import("tree.zig").Tree; 9 | 10 | /// Returns a repo's .git directory path 11 | pub fn repoToGitDir(allocator: mem.Allocator, repo_path: []const u8) ![]const u8 { 12 | return try fs.path.join(allocator, &.{ repo_path, ".git" }); 13 | } 14 | 15 | /// Find the root of the repository, caller responsible for memory. 16 | pub fn findRepoRoot(allocator: mem.Allocator) ![]const u8 { 17 | var path_buffer: [fs.MAX_PATH_BYTES]u8 = undefined; 18 | var dir = fs.cwd(); 19 | 20 | while (true) { 21 | const absolute_path = try dir.realpath(".", &path_buffer); 22 | 23 | dir.access(".git", .{}) catch |err| { 24 | switch (err) { 25 | error.FileNotFound => { 26 | if (mem.eql(u8, absolute_path, "/")) { 27 | return error.NoGitRepo; 28 | } 29 | 30 | var new_dir = try dir.openDir("..", .{}); 31 | // Can't close fs.cwd() or we get BADF 32 | if (dir.fd != fs.cwd().fd) { 33 | dir.close(); 34 | } 35 | 36 | dir = new_dir; 37 | continue; 38 | }, 39 | else => return err, 40 | } 41 | }; 42 | 43 | if (dir.fd != fs.cwd().fd) { 44 | dir.close(); 45 | } 46 | return allocator.dupe(u8, absolute_path); 47 | } 48 | } 49 | 50 | /// Returns the binary object name for a given hex string 51 | pub fn hexDigestToObjectName(hash: []const u8) ![20]u8 { 52 | var buffer: [20]u8 = undefined; 53 | const output = try std.fmt.hexToBytes(&buffer, hash); 54 | if (output.len != 20) { 55 | return error.IncorrectLength; 56 | } 57 | return buffer; 58 | } 59 | 60 | /// lessThan for strings for sorting 61 | pub fn lessThanStrings(context: void, lhs: []const u8, rhs: []const u8) bool { 62 | _ = context; 63 | return mem.lessThan(u8, lhs, rhs); 64 | } 65 | 66 | pub fn parseVariableLength(reader: anytype) !usize { 67 | var size: usize = 0; 68 | var shift: u6 = 0; 69 | var more = true; 70 | while (more) { 71 | var byte: VariableLengthByte = @bitCast(try reader.readByte()); 72 | size += @as(usize, byte.size) << shift; 73 | shift += 7; 74 | more = byte.more; 75 | } 76 | return size; 77 | } 78 | 79 | const VariableLengthByte = packed struct(u8) { 80 | size: u7, 81 | more: bool, 82 | }; 83 | 84 | pub const StringList = std.ArrayList([]const u8); 85 | pub const ObjectNameList = std.ArrayList([20]u8); 86 | -------------------------------------------------------------------------------- /src/index.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | const os = std.os; 4 | const mem = std.mem; 5 | const debug = std.debug; 6 | const testing = std.testing; 7 | 8 | const helpers = @import("helpers.zig"); 9 | const object_zig = @import("object.zig"); 10 | const repoToGitDir = helpers.repoToGitDir; 11 | const tree_zig = @import("tree.zig"); 12 | 13 | const saveObject = @import("object.zig").saveObject; 14 | 15 | /// Returns a repo's current index 16 | pub fn readIndex(allocator: mem.Allocator, repo_path: []const u8) !*Index { 17 | const index_path = try fs.path.join(allocator, &.{ repo_path, ".git", "index" }); 18 | defer allocator.free(index_path); 19 | 20 | const index_file = try fs.cwd().openFile(index_path, .{}); 21 | const index_data = try index_file.readToEndAlloc(allocator, std.math.maxInt(usize)); 22 | defer allocator.free(index_data); 23 | var index_hash: [20]u8 = undefined; 24 | std.crypto.hash.Sha1.hash(index_data[0..index_data.len-20], &index_hash, .{}); 25 | const index_signature = index_data[index_data.len-20..]; 26 | 27 | if (!mem.eql(u8, &index_hash, index_signature)) { 28 | return error.InvalidIndexSignature; 29 | } 30 | 31 | var index_buffer = std.io.fixedBufferStream(index_data[0..index_data.len-20]); 32 | const index_reader = index_buffer.reader(); 33 | 34 | const header = Index.Header{ 35 | .signature = try index_reader.readBytesNoEof(4), 36 | .version = try index_reader.readIntBig(u32), 37 | .entries = try index_reader.readIntBig(u32), 38 | }; 39 | 40 | if (!mem.eql(u8, "DIRC", &header.signature)) { 41 | return error.UnsupportedIndexSignature; 42 | } 43 | 44 | if (header.version > 3) { 45 | return error.UnsupportedIndexVersion; 46 | } 47 | 48 | var entries = Index.EntryList.init(allocator); 49 | 50 | var idx: usize = 0; 51 | while (idx < header.entries) : (idx += 1) { 52 | const entry_begin_pos = index_buffer.pos; 53 | 54 | const ctime_s = try index_reader.readIntBig(u32); 55 | const ctime_n = try index_reader.readIntBig(u32); 56 | const mtime_s = try index_reader.readIntBig(u32); 57 | const mtime_n = try index_reader.readIntBig(u32); 58 | const dev = try index_reader.readIntBig(u32); 59 | const ino = try index_reader.readIntBig(u32); 60 | const mode: Index.Mode = @bitCast(try index_reader.readIntBig(u32)); 61 | const uid = try index_reader.readIntBig(u32); 62 | const gid = try index_reader.readIntBig(u32); 63 | const file_size = try index_reader.readIntBig(u32); 64 | const object_name = try index_reader.readBytesNoEof(20); 65 | 66 | const flags: Index.Flags = @bitCast(try index_reader.readIntBig(u16)); 67 | const extended_flags = blk: { 68 | if (header.version > 2 and flags.extended) { 69 | const extra_flgs = try index_reader.readIntBig(u16); 70 | break :blk @as(Index.ExtendedFlags, @bitCast(extra_flgs)); 71 | } else { 72 | break :blk null; 73 | } 74 | }; 75 | 76 | const path = try index_reader.readUntilDelimiterAlloc(allocator, 0, fs.MAX_PATH_BYTES); 77 | 78 | const entry_end_pos = index_buffer.pos; 79 | const entry_size = entry_end_pos - entry_begin_pos; 80 | 81 | if (header.version < 4) { 82 | const extra_zeroes = (8 - (entry_size % 8)) % 8; 83 | var extra_zero_idx: usize = 0; 84 | while (extra_zero_idx < extra_zeroes) : (extra_zero_idx += 1) { 85 | if (try index_reader.readByte() != 0) { 86 | return error.InvalidEntryPathPadding; 87 | } 88 | } 89 | } 90 | 91 | const entry = Index.Entry{ 92 | .ctime_s = ctime_s, 93 | .ctime_n = ctime_n, 94 | .mtime_s = mtime_s, 95 | .mtime_n = mtime_n, 96 | .dev = dev, 97 | .ino = ino, 98 | .mode = mode, 99 | .uid = uid, 100 | .gid = gid, 101 | .file_size = file_size, 102 | .object_name = object_name, 103 | .flags = flags, 104 | .extended_flags = extended_flags, 105 | .path = path, 106 | }; 107 | 108 | try entries.append(entry); 109 | } 110 | 111 | const index = try allocator.create(Index); 112 | index.* = Index{ 113 | .header = header, 114 | .entries = entries, 115 | }; 116 | return index; 117 | } 118 | 119 | /// Writes the index to the repo's git folder 120 | pub fn writeIndex(allocator: mem.Allocator, repo_path: []const u8, index: *const Index) !void { 121 | const index_path = try fs.path.join(allocator, &.{ repo_path, ".git", "index" }); 122 | defer allocator.free(index_path); 123 | const index_file = try fs.cwd().createFile(index_path, .{ .read = true }); 124 | defer index_file.close(); 125 | 126 | const index_writer = index_file.writer(); 127 | 128 | try index_writer.writeAll(&index.header.signature); 129 | try index_writer.writeIntBig(u32, index.header.version); 130 | try index_writer.writeIntBig(u32, @as(u32, @truncate(index.entries.items.len))); 131 | 132 | var entries: []*const Index.Entry = try allocator.alloc(*Index.Entry, index.entries.items.len); 133 | defer allocator.free(entries); 134 | 135 | for (index.entries.items, 0..) |*entry, idx| { 136 | entries[idx] = entry; 137 | } 138 | 139 | mem.sort(*const Index.Entry, entries, {}, sortIndexEntries); 140 | 141 | for (entries) |entry| { 142 | var counter = std.io.countingWriter(index_writer); 143 | const counting_writer = counter.writer(); 144 | 145 | try counting_writer.writeIntBig(u32, entry.ctime_s); 146 | try counting_writer.writeIntBig(u32, entry.ctime_n); 147 | try counting_writer.writeIntBig(u32, entry.mtime_s); 148 | try counting_writer.writeIntBig(u32, entry.mtime_n); 149 | try counting_writer.writeIntBig(u32, entry.dev); 150 | try counting_writer.writeIntBig(u32, entry.ino); 151 | try counting_writer.writeIntBig(u32, @as(u32, @bitCast(entry.mode))); 152 | try counting_writer.writeIntBig(u32, entry.uid); 153 | try counting_writer.writeIntBig(u32, entry.gid); 154 | try counting_writer.writeIntBig(u32, entry.file_size); 155 | try counting_writer.writeAll(&entry.object_name); 156 | 157 | try counting_writer.writeIntBig(u16, @as(u16, @bitCast(entry.flags))); 158 | if (index.header.version > 2 and entry.flags.extended and entry.extended_flags != null) { 159 | try counting_writer.writeIntBig(u16, @as(u16, @bitCast(entry.extended_flags.?))); 160 | } 161 | 162 | try counting_writer.writeAll(entry.path); 163 | try counting_writer.writeByte(0); 164 | 165 | const entry_length = counter.bytes_written; 166 | if (index.header.version < 4) { 167 | const extra_zeroes = (8 - (entry_length % 8)) % 8; 168 | var extra_zeroes_idx: usize = 0; 169 | while (extra_zeroes_idx < extra_zeroes) : (extra_zeroes_idx += 1) { 170 | try counting_writer.writeByte(0); 171 | } 172 | } 173 | } 174 | 175 | try index_file.seekTo(0); 176 | var hasher = std.crypto.hash.Sha1.init(.{}); 177 | var pump_fifo = std.fifo.LinearFifo(u8, .{ .Static = 4086 }).init(); 178 | try pump_fifo.pump(index_file.reader(), hasher.writer()); 179 | var index_hash: [20]u8 = undefined; 180 | hasher.final(&index_hash); 181 | 182 | try index_file.seekFromEnd(0); 183 | try index_writer.writeAll(&index_hash); 184 | } 185 | 186 | pub fn sortIndexEntries(context: void, lhs: *const Index.Entry, rhs: *const Index.Entry) bool { 187 | _ = context; 188 | return mem.lessThan(u8, lhs.path, rhs.path); 189 | } 190 | 191 | // FIXME This is not super efficient. It traverses all directories, 192 | // including .git directories, and then rejects the files when it 193 | // calls `addFileToIndex`. I don't think there's a way to filter 194 | // directories using openIterableDir at the moment. 195 | /// Recursively add files to an index 196 | pub fn addFilesToIndex(allocator: mem.Allocator, repo_path: []const u8, index: *Index, dir_path: []const u8) !void { 197 | var dir_iterable = try fs.cwd().openIterableDir(dir_path, .{}); 198 | defer dir_iterable.close(); 199 | 200 | var walker = try dir_iterable.walk(allocator); 201 | defer walker.deinit(); 202 | 203 | while (try walker.next()) |walker_entry| { 204 | switch (walker_entry.kind) { 205 | .sym_link, .file => { 206 | const joined_path = try fs.path.join(allocator, &.{ dir_path, walker_entry.path }); 207 | defer allocator.free(joined_path); 208 | try addFileToIndex(allocator, repo_path, index, joined_path); 209 | }, 210 | .directory => continue, 211 | else => |tag| std.debug.print("Cannot add type {s} to index\n", .{ @tagName(tag) }), 212 | } 213 | } 214 | } 215 | 216 | /// Add the file at the path to an index 217 | pub fn addFileToIndex(allocator: mem.Allocator, repo_path: []const u8, index: *Index, file_path: []const u8) !void { 218 | var path_iter = mem.split(u8, file_path, fs.path.sep_str); 219 | while (path_iter.next()) |dir| { 220 | // Don't add .git files to the index 221 | if (mem.eql(u8, dir, ".git")) { 222 | return; 223 | } 224 | } 225 | 226 | const entry = try fileToIndexEntry(allocator, repo_path, file_path); 227 | errdefer entry.deinit(allocator); 228 | 229 | var replaced = false; 230 | 231 | for (index.entries.items) |*existing_entry| { 232 | if (mem.eql(u8, existing_entry.path, entry.path)) { 233 | std.debug.print("Replacing index entry at path {s}\n", .{ entry.path }); 234 | // Replace entry instead of removing old and adding new separately 235 | existing_entry.deinit(allocator); 236 | existing_entry.* = entry; 237 | replaced = true; 238 | break; 239 | } 240 | } 241 | 242 | if (!replaced) { 243 | try index.entries.append(entry); 244 | index.header.entries += 1; 245 | } 246 | } 247 | 248 | /// Reads file's details and returns a matching Index.Entry 249 | pub fn fileToIndexEntry(allocator: mem.Allocator, repo_path: []const u8, file_path: []const u8) !Index.Entry { 250 | const file = try fs.cwd().openFile(file_path, .{}); 251 | const stat = try os.fstat(file.handle); 252 | const absolute_repo_path = try fs.cwd().realpathAlloc(allocator, repo_path); 253 | defer allocator.free(absolute_repo_path); 254 | const absolute_file_path = try fs.cwd().realpathAlloc(allocator, file_path); 255 | defer allocator.free(absolute_file_path); 256 | const repo_relative_path = try fs.path.relative(allocator, absolute_repo_path, absolute_file_path); 257 | 258 | const data = try file.readToEndAlloc(allocator, std.math.maxInt(u32)); 259 | defer allocator.free(data); 260 | 261 | const name_len = blk: { 262 | if (repo_relative_path.len > 0xFFF) { 263 | break :blk @as(u12, 0xFFF); 264 | } else { 265 | break :blk @as(u12, @truncate(repo_relative_path.len)); 266 | } 267 | }; 268 | 269 | const git_dir_path = try repoToGitDir(allocator, repo_path); 270 | defer allocator.free(git_dir_path); 271 | 272 | const object_name = try saveObject(allocator, git_dir_path, data, .blob); 273 | 274 | const entry = Index.Entry{ 275 | .ctime_s = @intCast(stat.ctime().tv_sec), 276 | .ctime_n = @intCast(stat.ctime().tv_nsec), 277 | .mtime_s = @intCast(stat.mtime().tv_sec), 278 | .mtime_n = @intCast(stat.mtime().tv_nsec), 279 | .dev = @intCast(stat.dev), 280 | .ino = @intCast(stat.ino), 281 | .mode = @bitCast(@as(u32, stat.mode)), 282 | .uid = stat.uid, 283 | .gid = stat.gid, 284 | .file_size = @intCast(stat.size), 285 | .object_name = object_name, 286 | .flags = .{ 287 | .name_length = name_len, 288 | .stage = 0, 289 | .extended = false, 290 | .assume_valid = false, 291 | }, 292 | .extended_flags = null, 293 | .path = repo_relative_path, 294 | }; 295 | 296 | return entry; 297 | } 298 | 299 | pub fn removeFileFromIndex(allocator: mem.Allocator, repo_path: []const u8, index: *Index, file_path: []const u8) !void { 300 | const absolute_repo_path = try fs.cwd().realpathAlloc(allocator, repo_path); 301 | defer allocator.free(absolute_repo_path); 302 | const absolute_file_path = try fs.cwd().realpathAlloc(allocator, file_path); 303 | defer allocator.free(absolute_file_path); 304 | const repo_relative_path = try fs.path.relative(allocator, absolute_repo_path, absolute_file_path); 305 | defer allocator.free(repo_relative_path); 306 | 307 | var removed = false; 308 | for (index.entries.items, 0..) |entry, idx| { 309 | if (mem.eql(u8, entry.path, repo_relative_path)) { 310 | const removed_entry = index.entries.orderedRemove(idx); 311 | removed_entry.deinit(index.entries.allocator); 312 | removed = true; 313 | break; 314 | } 315 | } 316 | 317 | if (!removed) { 318 | return error.FileNotInIndex; 319 | } 320 | } 321 | 322 | pub const IndexList = std.ArrayList(usize); 323 | 324 | // https://git-scm.com/docs/index-format 325 | // TODO add extension support 326 | pub const Index = struct { 327 | header: Header, 328 | entries: EntryList, 329 | 330 | pub fn init(allocator: mem.Allocator) !*Index { 331 | const index = try allocator.create(Index); 332 | index.* = .{ 333 | .header = .{ 334 | .signature = "DIRC".*, 335 | .version = 2, 336 | .entries = 0, 337 | }, 338 | .entries = EntryList.init(allocator), 339 | }; 340 | return index; 341 | } 342 | 343 | pub fn deinit(self: *const Index) void { 344 | for (self.entries.items) |entry| { 345 | entry.deinit(self.entries.allocator); 346 | } 347 | self.entries.deinit(); 348 | self.entries.allocator.destroy(self); 349 | } 350 | 351 | pub const Header = struct { 352 | signature: [4]u8, 353 | version: u32, 354 | entries: u32, 355 | }; 356 | 357 | pub const Entry = struct { 358 | ctime_s: u32, 359 | ctime_n: u32, 360 | mtime_s: u32, 361 | mtime_n: u32, 362 | dev: u32, 363 | ino: u32, 364 | mode: Mode, 365 | uid: u32, 366 | gid: u32, 367 | file_size: u32, 368 | object_name: [20]u8, 369 | flags: Flags, 370 | extended_flags: ?ExtendedFlags, // v3+ and extended only 371 | path: []const u8, 372 | 373 | pub fn deinit(self: Entry, allocator: mem.Allocator) void { 374 | allocator.free(self.path); 375 | } 376 | 377 | pub fn format(self: Entry, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 378 | _ = fmt; 379 | _ = options; 380 | try out_stream.print("Index.Entry{{ mode: {o}, object_name: {s}, mode: {o}, size: {d:5}, path: {s} }}", 381 | .{ @as(u32, @bitCast(self.mode)), std.fmt.fmtSliceHexLower(&self.object_name), self.mode.unix_permissions, self.file_size, self.path }); 382 | } 383 | }; 384 | 385 | pub const EntryList = std.ArrayList(Entry); 386 | 387 | pub const Mode = packed struct(u32) { 388 | unix_permissions: u9, 389 | unused: u3 = 0, 390 | object_type: EntryType, 391 | padding: u16 = 0, 392 | }; 393 | 394 | pub const EntryType = enum(u4) { 395 | tree = 0o04, 396 | regular_file = 0o10, 397 | symbolic_link = 0o12, 398 | gitlink = 0o16, 399 | }; 400 | 401 | pub const Flags = packed struct(u16) { 402 | name_length: u12, 403 | stage: u2, 404 | extended: bool, 405 | assume_valid: bool, 406 | }; 407 | 408 | pub const ExtendedFlags = packed struct(u16) { 409 | unused: u13, 410 | intent_to_add: bool, 411 | skip_worktree: bool, 412 | reserved: bool, 413 | }; 414 | }; 415 | -------------------------------------------------------------------------------- /src/init.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | /// Initialize a normal git repo 9 | pub fn initialize(allocator: mem.Allocator, repo_path: []const u8) !void { 10 | const bare_path = try fs.path.join(allocator, &.{ repo_path, ".git" }); 11 | defer allocator.free(bare_path); 12 | 13 | try fs.cwd().makePath(repo_path); 14 | try initializeBare(allocator, bare_path); 15 | } 16 | 17 | /// Initialize a bare git repo (no index or working directory) 18 | pub fn initializeBare(allocator: mem.Allocator, repo_path: []const u8) !void { 19 | try fs.cwd().makeDir(repo_path); 20 | inline for (.{ "objects", "refs", "refs/heads" }) |dir| { 21 | const dir_path = try fs.path.join(allocator, &.{ repo_path, dir }); 22 | defer allocator.free(dir_path); 23 | try fs.cwd().makeDir(dir_path); 24 | } 25 | const head_path = try fs.path.join(allocator, &.{ repo_path, "HEAD" }); 26 | defer allocator.free(head_path); 27 | 28 | const head = try fs.cwd().createFile(head_path, .{}); 29 | try head.writeAll("ref: refs/heads/master\n"); 30 | defer head.close(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const mem = std.mem; 4 | const fs = std.fs; 5 | 6 | const object_zig = @import("object.zig"); 7 | const loadObject = object_zig.loadObject; 8 | const saveObject = object_zig.saveObject; 9 | const ObjectType = object_zig.ObjectType; 10 | const ObjectHeader = object_zig.ObjectHeader; 11 | 12 | const index_zig = @import("index.zig"); 13 | const Index = index_zig.Index; 14 | const readIndex = index_zig.readIndex; 15 | const writeIndex = index_zig.writeIndex; 16 | const addFilesToIndex = index_zig.addFilesToIndex; 17 | const addFileToIndex = index_zig.addFileToIndex; 18 | const removeFileFromIndex = index_zig.removeFileFromIndex; 19 | const modifiedFromIndex = index_zig.modifiedFromIndex; 20 | 21 | const helpers = @import("helpers.zig"); 22 | const repoToGitDir = helpers.repoToGitDir; 23 | const findRepoRoot = helpers.findRepoRoot; 24 | const ObjectNameList = helpers.ObjectNameList; 25 | const hexDigestToObjectName = helpers.hexDigestToObjectName; 26 | const lessThanStrings = helpers.lessThanStrings; 27 | 28 | const init_zig = @import("init.zig"); 29 | const initialize = init_zig.initialize; 30 | const initializeBare = init_zig.initializeBare; 31 | 32 | const tree_zig = @import("tree.zig"); 33 | const indexToTree = tree_zig.indexToTree; 34 | const TreeList = tree_zig.TreeList; 35 | const walkTree = tree_zig.walkTree; 36 | const readTree = tree_zig.readTree; 37 | const restoreTree = tree_zig.restoreTree; 38 | 39 | const commit_zig = @import("commit.zig"); 40 | const Commit = commit_zig.Commit; 41 | const writeCommit = commit_zig.writeCommit; 42 | const readCommit = commit_zig.readCommit; 43 | const restoreCommit = commit_zig.restoreCommit; 44 | 45 | const ref_zig = @import("ref.zig"); 46 | const resolveRef = ref_zig.resolveRef; 47 | const currentRef = ref_zig.currentRef; 48 | const updateHead = ref_zig.updateHead; 49 | const updateRef = ref_zig.updateRef; 50 | const currentHeadRef = ref_zig.currentHeadRef; 51 | const listHeadRefs = ref_zig.listHeadRefs; 52 | const listRefs = ref_zig.listRefs; 53 | const readRef = ref_zig.readRef; 54 | const resolveHead = ref_zig.resolveHead; 55 | const currentHead = ref_zig.currentHead; 56 | const resolveRefOrObjectName = ref_zig.resolveRefOrObjectName; 57 | const cannonicalizeRef = ref_zig.cannonicalizeRef; 58 | 59 | const tag_zig = @import("tag.zig"); 60 | const readTag = tag_zig.readTag; 61 | 62 | const status_zig = @import("status.zig"); 63 | const repoStatus = status_zig.repoStatus; 64 | 65 | const pack_zig = @import("pack.zig"); 66 | const Pack = pack_zig.Pack; 67 | 68 | const pack_index_zig = @import("pack_index.zig"); 69 | const PackIndex = pack_index_zig.PackIndex; 70 | 71 | pub fn main() !void { 72 | var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; 73 | var allocator = gpa.allocator(); 74 | defer _ = gpa.deinit(); 75 | 76 | var args = std.process.args(); 77 | _ = args.next(); 78 | 79 | const subcommand = blk: { 80 | const str = args.next() orelse break :blk null; 81 | break :blk std.meta.stringToEnum(SubCommands, str); 82 | } orelse { 83 | std.debug.print("No subcommand specified.\nAvailable subcommands:\n", .{}); 84 | for (std.meta.fieldNames(SubCommands)) |field| { 85 | std.debug.print("{s}\n", .{field}); 86 | } 87 | return; 88 | }; 89 | 90 | switch (subcommand) { 91 | .index => { 92 | const repo_root = try findRepoRoot(allocator); 93 | defer allocator.free(repo_root); 94 | 95 | std.debug.print("Repo root: {s}\n", .{ repo_root }); 96 | const index = readIndex(allocator, repo_root) catch |err| switch (err) { 97 | error.FileNotFound => { 98 | std.debug.print("No index\n", .{}); 99 | return; 100 | }, 101 | else => return err, 102 | }; 103 | std.debug.print("Signature: {s}\nNum Entries: {d}\nVersion: {d}\n", .{ index.header.signature, index.header.entries, index.header.version }); 104 | for (index.entries.items) |entry| { 105 | std.debug.print("{}\n", .{ entry }); 106 | } 107 | defer index.deinit(); 108 | return; 109 | }, 110 | .init => { 111 | const path = blk: { 112 | if (args.next()) |valid_path| { 113 | break :blk valid_path; 114 | } else { 115 | break :blk "."; 116 | } 117 | }; 118 | try initialize(allocator, path); 119 | std.debug.print("initialized empty repository {s}\n", .{ path }); 120 | }, 121 | .add => { 122 | const file_path = blk: { 123 | if (args.next()) |valid_path| { 124 | break :blk valid_path; 125 | } 126 | std.debug.print("Must specify file path\n", .{}); 127 | return error.NoFilePath; 128 | }; 129 | 130 | const repo_path = try findRepoRoot(allocator); 131 | defer allocator.free(repo_path); 132 | 133 | var index = readIndex(allocator, repo_path) catch |err| switch (err) { 134 | error.FileNotFound => try Index.init(allocator), 135 | else => return err, 136 | }; 137 | defer index.deinit(); 138 | 139 | const stat = try fs.cwd().statFile(file_path); 140 | switch (stat.kind) { 141 | .directory => try addFilesToIndex(allocator, repo_path, index, file_path), 142 | .sym_link, .file, => try addFileToIndex(allocator, repo_path, index, file_path), 143 | else => |tag| std.debug.print("Cannot add file of type {s} to index\n", .{ @tagName(tag) }), 144 | } 145 | 146 | try writeIndex(allocator, repo_path, index); 147 | }, 148 | .commit => { 149 | const repo_root = try findRepoRoot(allocator); 150 | defer allocator.free(repo_root); 151 | 152 | const git_dir_path = try repoToGitDir(allocator, repo_root); 153 | defer allocator.free(git_dir_path); 154 | 155 | const tree = try indexToTree(allocator, repo_root); 156 | const committer = Commit.Committer{ 157 | .name = "Gaba Goul", 158 | .email = "gaba@cool.ca", 159 | .time = std.time.timestamp(), 160 | .timezone = 0, 161 | }; 162 | var parents = ObjectNameList.init(allocator); 163 | defer parents.deinit(); 164 | 165 | const head_ref = try resolveRef(allocator, git_dir_path, "HEAD"); 166 | if (head_ref) |valid_ref| { 167 | try parents.append(valid_ref); 168 | } 169 | 170 | var commit = Commit{ 171 | .allocator = allocator, 172 | .tree = tree, 173 | .parents = parents, 174 | .author = committer, 175 | .committer = committer, 176 | .message = "Commit test!", 177 | }; 178 | 179 | const object_name = try writeCommit(allocator, git_dir_path, commit); 180 | 181 | if (try currentRef(allocator, git_dir_path)) |current_ref| { 182 | defer allocator.free(current_ref); 183 | std.debug.print("Commit {s} to {s}\n", .{ std.fmt.fmtSliceHexLower(&object_name), current_ref }); 184 | 185 | try updateRef(allocator, git_dir_path, current_ref, .{ .object_name = object_name }); 186 | } else { 187 | std.debug.print("Warning: In a detached HEAD state\n", .{}); 188 | std.debug.print("Commit {s}\n", .{ std.fmt.fmtSliceHexLower(&object_name) }); 189 | 190 | try updateRef(allocator, git_dir_path, "HEAD", .{ .object_name = object_name }); 191 | } 192 | }, 193 | .branches => { 194 | const repo_path = try findRepoRoot(allocator); 195 | defer allocator.free(repo_path); 196 | 197 | const git_dir_path = try repoToGitDir(allocator, repo_path); 198 | defer allocator.free(git_dir_path); 199 | 200 | const current_ref = try currentHeadRef(allocator, git_dir_path); 201 | 202 | defer if (current_ref) |valid_ref| allocator.free(valid_ref); 203 | 204 | var refs = try listHeadRefs(allocator, git_dir_path); 205 | defer refs.deinit(); 206 | 207 | mem.sort([]const u8, refs.refs, {}, lessThanStrings); 208 | 209 | for (refs.refs) |ref| { 210 | const indicator: u8 = blk: { 211 | if (current_ref) |valid_ref| { 212 | break :blk if (mem.eql(u8, valid_ref, ref)) '*' else ' '; 213 | } else break :blk ' '; 214 | }; 215 | std.debug.print("{c} {s}\n", .{ indicator, ref }); 216 | } 217 | }, 218 | .@"branch-create" => { 219 | const new_branch_name = args.next() orelse { 220 | std.debug.print("No branch name specified\n", .{}); 221 | return; 222 | }; 223 | 224 | const repo_path = try findRepoRoot(allocator); 225 | defer allocator.free(repo_path); 226 | 227 | const git_dir_path = try repoToGitDir(allocator, repo_path); 228 | defer allocator.free(git_dir_path); 229 | 230 | const current_commit = try resolveRef(allocator, git_dir_path, "HEAD"); 231 | if (current_commit) |valid_commit_object_name| { 232 | try updateRef(allocator, git_dir_path, new_branch_name, .{ .object_name = valid_commit_object_name }); 233 | } 234 | try updateRef(allocator, git_dir_path, "HEAD", .{ .ref = new_branch_name }); 235 | }, 236 | .refs => { 237 | const repo_path = try findRepoRoot(allocator); 238 | defer allocator.free(repo_path); 239 | 240 | const git_dir_path = try repoToGitDir(allocator, repo_path); 241 | defer allocator.free(git_dir_path); 242 | 243 | const refs = try listRefs(allocator, git_dir_path); 244 | defer refs.deinit(); 245 | 246 | for (refs.refs) |ref| { 247 | std.debug.print("{s}\n", .{ ref }); 248 | } 249 | }, 250 | .@"read-object" => { 251 | const blob_digest = args.next() orelse { 252 | std.debug.print("No blob object name specified\n", .{}); 253 | return; 254 | }; 255 | 256 | const object_name = try hexDigestToObjectName(blob_digest); 257 | 258 | const repo_path = try findRepoRoot(allocator); 259 | defer allocator.free(repo_path); 260 | 261 | const git_dir_path = try repoToGitDir(allocator, repo_path); 262 | defer allocator.free(git_dir_path); 263 | 264 | const stdout = std.io.getStdOut().writer(); 265 | _ = try loadObject(allocator, git_dir_path, object_name, stdout); 266 | }, 267 | .@"read-tree" => { 268 | const tree_hash_digest = args.next() orelse { 269 | std.debug.print("No tree object name specified\n", .{}); 270 | return; 271 | }; 272 | 273 | const repo_path = try findRepoRoot(allocator); 274 | defer allocator.free(repo_path); 275 | 276 | const git_dir_path = try repoToGitDir(allocator, repo_path); 277 | defer allocator.free(git_dir_path); 278 | 279 | var tree_name_buffer: [20]u8 = undefined; 280 | const tree_object_name = try std.fmt.hexToBytes(&tree_name_buffer, tree_hash_digest); 281 | _ = tree_object_name; 282 | 283 | var walker = try walkTree(allocator, git_dir_path, tree_name_buffer); 284 | defer walker.deinit(); 285 | 286 | while (try walker.next()) |entry| { 287 | std.debug.print("{}\n", .{ entry }); 288 | } 289 | }, 290 | .@"read-commit" => { 291 | const commit_hash_digest = args.next() orelse { 292 | std.debug.print("No commit object hash specified\n", .{}); 293 | return; 294 | }; 295 | 296 | const repo_path = try findRepoRoot(allocator); 297 | defer allocator.free(repo_path); 298 | 299 | const git_dir_path = try repoToGitDir(allocator, repo_path); 300 | defer allocator.free(git_dir_path); 301 | 302 | const commit_object_name = try hexDigestToObjectName(commit_hash_digest); 303 | const commit = try readCommit(allocator, git_dir_path, commit_object_name); 304 | defer commit.deinit(); 305 | 306 | std.debug.print("{any}\n", .{ commit }); 307 | }, 308 | .root => { 309 | const repo_path = try findRepoRoot(allocator); 310 | defer allocator.free(repo_path); 311 | 312 | std.debug.print("{s}\n", .{ repo_path }); 313 | }, 314 | .@"read-ref" => { 315 | const ref_name = args.next(); 316 | 317 | const repo_path = try findRepoRoot(allocator); 318 | defer allocator.free(repo_path); 319 | 320 | const git_dir_path = try repoToGitDir(allocator, repo_path); 321 | defer allocator.free(git_dir_path); 322 | 323 | if (ref_name) |valid_ref_name| { 324 | const ref = try readRef(allocator, git_dir_path, valid_ref_name) orelse return; 325 | defer ref.deinit(allocator); 326 | 327 | std.debug.print("{}\n", .{ ref }); 328 | } else { 329 | const ref_list = try listRefs(allocator, git_dir_path); 330 | defer ref_list.deinit(); 331 | 332 | for (ref_list.refs) |ref_path| { 333 | const ref = try readRef(allocator, git_dir_path, ref_path); 334 | defer ref.?.deinit(allocator); 335 | 336 | std.debug.print("{s}: {}\n", .{ ref_path, ref.? }); 337 | } 338 | } 339 | }, 340 | .@"read-tag" => { 341 | const tag_name = args.next() orelse { 342 | std.debug.print("No tag specified\n", .{}); 343 | return; 344 | }; 345 | 346 | const repo_path = try findRepoRoot(allocator); 347 | defer allocator.free(repo_path); 348 | 349 | const git_dir_path = try repoToGitDir(allocator, repo_path); 350 | defer allocator.free(git_dir_path); 351 | 352 | const tag_object_name = try hexDigestToObjectName(tag_name); 353 | 354 | const tag = try readTag(allocator, git_dir_path, tag_object_name); 355 | defer tag.deinit(); 356 | 357 | std.debug.print("{}\n", .{ tag }); 358 | }, 359 | .@"read-pack" => { 360 | const pack_name = args.next() orelse { 361 | std.debug.print("No pack specified\n", .{}); 362 | return; 363 | }; 364 | 365 | const repo_path = try findRepoRoot(allocator); 366 | defer allocator.free(repo_path); 367 | 368 | const git_dir_path = try repoToGitDir(allocator, repo_path); 369 | defer allocator.free(git_dir_path); 370 | 371 | const packfile_name = try std.fmt.allocPrint(allocator, "pack-{s}.pack", .{ pack_name }); 372 | defer allocator.free(packfile_name); 373 | 374 | const pack_path = try fs.path.join(allocator, &.{ git_dir_path, "objects", "pack", packfile_name }); 375 | defer allocator.free(pack_path); 376 | 377 | const pack = try Pack.init(allocator, pack_path); 378 | defer pack.deinit(); 379 | 380 | std.debug.print("{any}\n", .{ pack }); 381 | 382 | // try pack.validate(); 383 | 384 | var iter = try pack.iterator(); 385 | defer iter.deinit(); 386 | 387 | while (try iter.next()) |entry| { 388 | std.debug.print("{s} ({d}): {any}\n", .{ std.fmt.fmtSliceHexLower(&entry.object_name), entry.offset, entry.object_reader.header }); 389 | } 390 | }, 391 | .@"read-pack-index" => { 392 | const pack_index_name = args.next() orelse { 393 | std.debug.print("No pack specified\n", .{}); 394 | return; 395 | }; 396 | 397 | const pack_search = args.next() orelse { 398 | std.debug.print("No search name\n", .{}); 399 | return; 400 | }; 401 | 402 | const repo_path = try findRepoRoot(allocator); 403 | defer allocator.free(repo_path); 404 | 405 | const git_dir_path = try repoToGitDir(allocator, repo_path); 406 | defer allocator.free(git_dir_path); 407 | 408 | const index_file_name = try std.fmt.allocPrint(allocator, "pack-{s}.idx", .{ pack_index_name }); 409 | defer allocator.free(index_file_name); 410 | 411 | const pack_path = try fs.path.join(allocator, &.{ git_dir_path, "objects", "pack", index_file_name }); 412 | defer allocator.free(pack_path); 413 | 414 | const pack_index = try PackIndex.init(pack_path); 415 | defer pack_index.deinit(); 416 | 417 | const name = try helpers.hexDigestToObjectName(pack_search); 418 | if (try pack_index.find(name)) |offset| { 419 | std.debug.print("offset: {d}\n", .{offset}); 420 | } else { 421 | std.debug.print("offset not found\n", .{}); 422 | } 423 | }, 424 | .log => { 425 | const repo_path = try findRepoRoot(allocator); 426 | defer allocator.free(repo_path); 427 | 428 | const git_dir_path = try repoToGitDir(allocator, repo_path); 429 | defer allocator.free(git_dir_path); 430 | 431 | const commit_object_name = try resolveHead(allocator, git_dir_path) orelse return; 432 | 433 | var commit: ?*Commit = null; 434 | commit = try readCommit(allocator, git_dir_path, commit_object_name); 435 | 436 | std.debug.print("{s}: ", .{std.fmt.fmtSliceHexLower(&commit_object_name)}); 437 | while (commit) |valid_commit| { 438 | const old_commit = valid_commit; 439 | defer old_commit.deinit(); 440 | 441 | std.debug.print("{}\n", .{valid_commit}); 442 | if (valid_commit.parents.items.len >= 1) { 443 | // HACK We only look at the first parent, we should 444 | // look at all (for merges, etc.) 445 | std.debug.print("{s}: ", .{std.fmt.fmtSliceHexLower(&valid_commit.parents.items[0])}); 446 | commit = try readCommit(allocator, git_dir_path, valid_commit.parents.items[0]); 447 | } else { 448 | commit = null; 449 | } 450 | } 451 | }, 452 | .@"search-pack" => { 453 | const pack_search = args.next() orelse { 454 | std.debug.print("No search name\n", .{}); 455 | return; 456 | }; 457 | 458 | const object_name = try hexDigestToObjectName(pack_search); 459 | 460 | const repo_path = try findRepoRoot(allocator); 461 | defer allocator.free(repo_path); 462 | 463 | const git_dir_path = try repoToGitDir(allocator, repo_path); 464 | defer allocator.free(git_dir_path); 465 | 466 | const result = try pack_index_zig.searchPackIndicies(allocator, git_dir_path, object_name); 467 | std.debug.print("Pack: {s}, Offset: {d}\n", .{ std.fmt.fmtSliceHexLower(&result.pack), result.offset }); 468 | }, 469 | .status => { 470 | // TODO Give more useful status information 471 | 472 | const repo_path = try findRepoRoot(allocator); 473 | defer allocator.free(repo_path); 474 | 475 | const git_dir_path = try repoToGitDir(allocator, repo_path); 476 | defer allocator.free(git_dir_path); 477 | 478 | const current_ref = try currentHead(allocator, git_dir_path); 479 | if (current_ref) |valid_ref| { 480 | defer valid_ref.deinit(allocator); 481 | 482 | switch (valid_ref) { 483 | .ref => |ref| std.debug.print("On branch {s}\n", .{ std.mem.trimLeft(u8, ref, "refs/heads/") }), 484 | .object_name => |object_name| std.debug.print("Detached HEAD {s}\n", .{ std.fmt.fmtSliceHexLower(&object_name) }), 485 | } 486 | } 487 | const modifed_from_index = try repoStatus(allocator, repo_path); 488 | defer modifed_from_index.deinit(); 489 | 490 | std.debug.print("\n", .{}); 491 | var clean = true; 492 | for (modifed_from_index.entries.items) |entry| { 493 | if (entry.status != .staged_added) { 494 | continue; 495 | } 496 | clean = false; 497 | std.debug.print("{s}: {s}: {s}\n", .{ @tagName(entry.status), entry.path, std.fmt.fmtSliceHexLower(&entry.object_name.?) }); 498 | } 499 | for (modifed_from_index.entries.items) |entry| { 500 | if (entry.status != .staged_modified) { 501 | continue; 502 | } 503 | clean = false; 504 | std.debug.print("{s}: {s}: {s}\n", .{ @tagName(entry.status), entry.path, std.fmt.fmtSliceHexLower(&entry.object_name.?) }); 505 | } 506 | for (modifed_from_index.entries.items) |entry| { 507 | if (entry.status != .staged_removed) { 508 | continue; 509 | } 510 | clean = false; 511 | std.debug.print("{s}: {s}: {s}\n", .{ @tagName(entry.status), entry.path, std.fmt.fmtSliceHexLower(&entry.object_name.?) }); 512 | } 513 | for (modifed_from_index.entries.items) |entry| { 514 | if (entry.status != .modified) { 515 | continue; 516 | } 517 | clean = false; 518 | std.debug.print("{s}: {s}: {s}\n", .{ @tagName(entry.status), entry.path, std.fmt.fmtSliceHexLower(&entry.object_name.?) }); 519 | } 520 | for (modifed_from_index.entries.items) |entry| { 521 | if (entry.status != .removed) { 522 | continue; 523 | } 524 | clean = false; 525 | std.debug.print("{s}: {s}: {s}\n", .{ @tagName(entry.status), entry.path, std.fmt.fmtSliceHexLower(&entry.object_name.?) }); 526 | } 527 | // TODO commented out until .gitignore works, too many 528 | // junk files displayed 529 | // 530 | // for (modifed_from_index.entries.items) |entry| { 531 | // if (entry.status != .untracked) { 532 | // continue; 533 | // } 534 | // std.debug.print("{s}: {s}\n", .{ @tagName(entry.status), entry.path }); 535 | // } 536 | if (clean) { 537 | std.debug.print("Clean working tree\n", .{}); 538 | } 539 | }, 540 | .rm => { 541 | const file_path = blk: { 542 | if (args.next()) |valid_path| { 543 | break :blk valid_path; 544 | } 545 | std.debug.print("Must specify file path\n", .{}); 546 | return error.NoFilePath; 547 | }; 548 | 549 | const repo_path = try findRepoRoot(allocator); 550 | defer allocator.free(repo_path); 551 | 552 | var index = try readIndex(allocator, repo_path); 553 | defer index.deinit(); 554 | 555 | try removeFileFromIndex(allocator, repo_path, index, file_path); 556 | 557 | try writeIndex(allocator, repo_path, index); 558 | }, 559 | .checkout => { 560 | const ref_or_object_name = args.next() orelse { 561 | std.debug.print("No commit specified\n", .{}); 562 | return; 563 | }; 564 | 565 | const repo_path = try findRepoRoot(allocator); 566 | defer allocator.free(repo_path); 567 | 568 | const git_dir_path = try repoToGitDir(allocator, repo_path); 569 | defer allocator.free(git_dir_path); 570 | 571 | const commit_object_name = try resolveRefOrObjectName(allocator, git_dir_path, ref_or_object_name) orelse { 572 | std.debug.print("Invalid ref or commit hash\n", .{}); 573 | return; 574 | }; 575 | 576 | const new_ref = try cannonicalizeRef(allocator, git_dir_path, ref_or_object_name) orelse { 577 | std.debug.print("Invalid ref or commit hash\n", .{}); 578 | return; 579 | }; 580 | defer new_ref.deinit(allocator); 581 | 582 | // TODO doesn't remove files that were added since 583 | // restored commit 584 | const new_index = try restoreCommit(allocator, repo_path, commit_object_name); 585 | defer new_index.deinit(); 586 | 587 | try writeIndex(allocator, repo_path, new_index); 588 | try updateHead(allocator, git_dir_path, new_ref); 589 | } 590 | } 591 | } 592 | 593 | const SubCommands = enum { 594 | add, 595 | branches, 596 | @"branch-create", 597 | checkout, 598 | commit, 599 | index, 600 | init, 601 | log, 602 | @"read-commit", 603 | @"read-object", 604 | @"read-pack", 605 | @"read-pack-index", 606 | @"read-ref", 607 | @"read-tag", 608 | @"read-tree", 609 | refs, 610 | rm, 611 | root, 612 | @"search-pack", 613 | status, 614 | // TODO notes 615 | }; 616 | 617 | /// TODO Restores the contents of a file from a commit and a path 618 | pub fn restoreFileFromCommit(allocator: mem.Allocator, git_dir_path: []const u8, path: []const u8) !void { 619 | _ = git_dir_path; 620 | _ = allocator; 621 | const file = try fs.cwd().createFile(path, .{}); 622 | defer file.close(); 623 | } 624 | 625 | test "ref all" { 626 | std.testing.refAllDecls(@This()); 627 | } 628 | -------------------------------------------------------------------------------- /src/object.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | const mem = std.mem; 4 | const debug = std.debug; 5 | const testing = std.testing; 6 | 7 | const pack_zig = @import("pack.zig"); 8 | const pack_index_zig = @import("pack_index.zig"); 9 | const pack_delta_zig = @import("pack_delta.zig"); 10 | const DeltaInstructions = pack_delta_zig.DeltaInstructions; 11 | 12 | /// Calculate the object name of a file 13 | pub fn hashFile(file: fs.File) ![20]u8 { 14 | var hash_buffer: [20]u8 = undefined; 15 | var hash = std.crypto.hash.Sha1.init(.{}); 16 | const hash_writer = hash.writer(); 17 | const seekable = file.seekableStream(); 18 | const file_reader = file.reader(); 19 | try hash_writer.print("{s} {d}\x00", .{ @tagName(ObjectType.blob), try seekable.getEndPos() }); 20 | const Pump = std.fifo.LinearFifo(u8, .{ .Static = 4096 }); 21 | var pump = Pump.init(); 22 | try seekable.seekTo(0); 23 | try pump.pump(file_reader, hash_writer); 24 | hash.final(&hash_buffer); 25 | return hash_buffer; 26 | } 27 | 28 | /// Hashes data and returns its object name 29 | pub fn hashObject(data: []const u8, obj_type: ObjectType, digest: *[20]u8) void { 30 | var hash = std.crypto.hash.Sha1.init(.{}); 31 | const writer = hash.writer(); 32 | try writer.print("{s} {d}\x00", .{ @tagName(obj_type), data.len }); 33 | try writer.writeAll(data); 34 | hash.final(digest); 35 | } 36 | 37 | /// Restores the contents of a file from an object 38 | pub fn restoreFileFromObject(allocator: mem.Allocator, git_dir_path: []const u8, path: []const u8, object_name: [20]u8) !ObjectHeader { 39 | const file = try fs.cwd().createFile(path, .{}); 40 | defer file.close(); 41 | 42 | const writer = file.writer(); 43 | return try loadObject(allocator, git_dir_path, object_name, writer); 44 | } 45 | 46 | // TODO Maybe rewrite to use a reader interface instead of a data slice 47 | /// Writes data to an object and returns its object name 48 | pub fn saveObject(allocator: mem.Allocator, git_dir_path: []const u8, data: []const u8, obj_type: ObjectType) ![20]u8 { 49 | var digest: [20]u8 = undefined; 50 | hashObject(data, obj_type, &digest); 51 | 52 | const hex_digest = try std.fmt.allocPrint(allocator, "{s}", .{ std.fmt.fmtSliceHexLower(&digest) }); 53 | defer allocator.free(hex_digest); 54 | 55 | const path = try fs.path.join(allocator, &.{ git_dir_path, "objects", hex_digest[0..2], hex_digest[2..] }); 56 | defer allocator.free(path); 57 | try fs.cwd().makePath(fs.path.dirname(path).?); 58 | 59 | const file = try fs.cwd().createFile(path, .{}); 60 | defer file.close(); 61 | 62 | // try zlibStreamWriter(allocator, file.writer(), .{}); 63 | var compressor = try std.compress.zlib.compressStream(allocator, file.writer(), .{}); 64 | defer compressor.deinit(); 65 | 66 | const writer = compressor.writer(); 67 | try writer.print("{s} {d}\x00", .{ @tagName(obj_type), data.len }); 68 | try writer.writeAll(data); 69 | try compressor.finish(); 70 | 71 | return digest; 72 | } 73 | 74 | /// Returns a reader for an object's data 75 | pub fn looseObjectReader(allocator: mem.Allocator, git_dir_path: []const u8, object_name: [20]u8) !?LooseObjectReader { 76 | return LooseObjectReader.init(allocator, git_dir_path, object_name); 77 | } 78 | 79 | pub const LooseObjectReader = struct { 80 | decompressor: Decompressor, 81 | file: fs.File, 82 | header: ObjectHeader, 83 | 84 | pub const Decompressor = std.compress.zlib.DecompressStream(fs.File.Reader); 85 | pub const Reader = Decompressor.Reader; 86 | const Self = @This(); 87 | 88 | pub fn reader(self: *Self) Reader { 89 | return self.decompressor.reader(); 90 | } 91 | 92 | pub fn init(allocator: mem.Allocator, git_dir_path: []const u8, object_name: [20]u8) !?LooseObjectReader { 93 | var hex_buffer: [40]u8 = undefined; 94 | const hex_digest = try std.fmt.bufPrint(&hex_buffer, "{s}", .{ std.fmt.fmtSliceHexLower(&object_name) }); 95 | 96 | const path = try fs.path.join(allocator, &.{ git_dir_path, "objects", hex_digest[0..2], hex_digest[2..] }); 97 | defer allocator.free(path); 98 | 99 | const file = fs.cwd().openFile(path, .{}) catch |err| switch (err) { 100 | error.FileNotFound => return null, 101 | else => return err, 102 | }; 103 | 104 | var decompressor = try std.compress.zlib.decompressStream(allocator, file.reader()); 105 | const decompressor_reader = decompressor.reader(); 106 | 107 | const header = try decompressor_reader.readUntilDelimiterAlloc(allocator, 0, 1024); 108 | defer allocator.free(header); 109 | 110 | var header_iter = mem.split(u8, header, " "); 111 | const object_type = std.meta.stringToEnum(ObjectType, header_iter.first()) orelse return error.InvalidObjectType; 112 | const size = blk: { 113 | const s = header_iter.next() orelse return error.InvalidObjectSize; 114 | const n = try std.fmt.parseInt(usize, s, 10); 115 | break :blk n; 116 | }; 117 | 118 | const object_header = ObjectHeader{ 119 | .type = object_type, 120 | .size = size, 121 | }; 122 | 123 | return .{ 124 | .decompressor = decompressor, 125 | .file = file, 126 | .header = object_header, 127 | }; 128 | } 129 | 130 | pub fn deinit(self: *Self) void { 131 | self.decompressor.deinit(); 132 | self.file.close(); 133 | } 134 | }; 135 | 136 | /// Writes the data from an object to a writer 137 | pub fn loadObject(allocator: mem.Allocator, git_dir_path: []const u8, object_name: [20]u8, writer: anytype) !ObjectHeader { 138 | var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); 139 | 140 | var loose_object_reader = try looseObjectReader(allocator, git_dir_path, object_name); 141 | if (loose_object_reader) |*object_reader| { 142 | defer object_reader.deinit(); 143 | 144 | const reader = object_reader.reader(); 145 | 146 | try fifo.pump(reader, writer); 147 | 148 | return object_reader.header; 149 | } 150 | 151 | var pack_object_reader = try pack_zig.packObjectReader(allocator, git_dir_path, object_name); 152 | var pack_object_reader_valid = true; 153 | defer { 154 | // This is a hack but it works 155 | if (pack_object_reader_valid) 156 | pack_object_reader.deinit(); 157 | } 158 | 159 | const pack_reader = pack_object_reader.reader(); 160 | const pack_object_type = pack_object_reader.object_reader.header.type; 161 | 162 | if (packObjectTypeToObjectType(pack_object_type)) |object_type| { 163 | try fifo.pump(pack_reader, writer); 164 | 165 | return ObjectHeader{ 166 | .type = object_type, 167 | .size = pack_object_reader.object_reader.header.size, 168 | }; 169 | } 170 | 171 | var delta_stack = DeltaStack.init(allocator); 172 | defer delta_stack.deinit(); 173 | 174 | const first_instructions = try pack_delta_zig.parseDeltaInstructions(allocator, pack_object_reader.object_reader.header.size, pack_reader); 175 | try delta_stack.push(pack_object_reader.object_reader.header); 176 | delta_stack.last().instructions = first_instructions; 177 | 178 | // we gotta keep going down the stack until we reach a non-delta, 179 | // then unwind the stack and apply the deltas... 180 | while (delta_stack.last().header.delta) |ref_type| { 181 | var instructions = delta_stack.last().instructions.?; 182 | 183 | if (ref_type == .ref) { 184 | std.debug.print("Ref delta: {s}\n", .{ std.fmt.fmtSliceHexLower(&ref_type.ref) }); 185 | pack_object_reader.deinit(); 186 | pack_object_reader_valid = false; 187 | // I would be shocked if a pack ever referred to a loose object 188 | pack_object_reader = try pack_zig.packObjectReader(allocator, git_dir_path, ref_type.ref); 189 | pack_object_reader_valid = true; 190 | } else { 191 | const object_offset = pack_object_reader.object_reader.offset - ref_type.offset; 192 | std.debug.print("Ofs delta: {d}, current: {d}\n", .{object_offset, pack_object_reader.object_reader.offset}); 193 | try pack_object_reader.setOffset(object_offset); 194 | } 195 | 196 | const base_object_header = pack_object_reader.object_reader.header; 197 | try delta_stack.push(base_object_header); 198 | 199 | if (packObjectTypeToObjectType(base_object_header.type) == null) { 200 | // another delta 201 | const base_object_buffer = try allocator.alloc(u8, instructions.base_size); 202 | defer allocator.free(base_object_buffer); 203 | 204 | var base_object_stream = std.io.fixedBufferStream(base_object_buffer); 205 | try fifo.pump(pack_object_reader.reader(), base_object_stream.writer()); 206 | 207 | var new_instructions = try pack_delta_zig.parseDeltaInstructions(allocator, base_object_header.size, base_object_stream.reader()); 208 | delta_stack.last().instructions = new_instructions; 209 | } else { 210 | // finally base object 211 | try fifo.pump(pack_object_reader.reader(), delta_stack.last().base_object.writer()); 212 | } 213 | } 214 | 215 | // @panic("we made it"); 216 | // var buffer_stream = delta_stack.last().bufferStream(); 217 | // try fifo.pump(buffer_stream.reader(), writer); 218 | return error.What; 219 | } 220 | 221 | pub const DeltaStack = struct { 222 | stack: std.ArrayList(DeltaFrame), 223 | 224 | pub fn init(allocator: mem.Allocator) DeltaStack { 225 | return .{ .stack = std.ArrayList(DeltaFrame).init(allocator) }; 226 | } 227 | 228 | pub fn last(self: DeltaStack) *DeltaFrame { 229 | return &self.stack.items[self.stack.items.len-1]; 230 | } 231 | 232 | pub fn second_last(self: DeltaStack) *DeltaFrame { 233 | return &self.stack.items[self.stack.items.len-2]; 234 | } 235 | 236 | pub fn count(self: DeltaStack) usize { 237 | return self.stack.items.len; 238 | } 239 | 240 | pub fn push(self: *DeltaStack, header: pack_zig.ObjectHeader) !void { 241 | try self.stack.append(.{ 242 | .instructions = null, 243 | .base_object = std.ArrayList(u8).init(self.stack.allocator), 244 | .header = header, 245 | }); 246 | } 247 | 248 | pub fn pop(self: *DeltaStack) void { 249 | var last_frame = self.last(); 250 | if (last_frame.instructions) |instructions| 251 | instructions.deinit(); 252 | last_frame.base_object.deinit(); 253 | _ = self.stack.pop(); 254 | } 255 | 256 | pub fn deinit(self: *DeltaStack) void { 257 | while (self.count() > 0) { 258 | self.pop(); 259 | } 260 | self.stack.deinit(); 261 | } 262 | }; 263 | 264 | pub const DeltaFrame = struct { 265 | instructions: ?DeltaInstructions, 266 | base_object: std.ArrayList(u8), 267 | header: pack_zig.ObjectHeader, 268 | 269 | const BufferStream = std.io.FixedBufferStream([]const u8); 270 | 271 | pub fn bufferStream(self: DeltaFrame) BufferStream { 272 | return std.io.fixedBufferStream(self.expanded_object); 273 | } 274 | }; 275 | 276 | pub fn packObjectTypeToObjectType(pack_object_type: pack_zig.PackObjectType) ?ObjectType { 277 | return switch (pack_object_type) { 278 | .commit => .commit, 279 | .tree => .tree, 280 | .blob => .blob, 281 | .tag => .tag, 282 | else => null, 283 | }; 284 | } 285 | 286 | pub const ObjectHeader = struct { 287 | type: ObjectType, 288 | size: usize, 289 | }; 290 | 291 | pub const ObjectType = enum { 292 | blob, 293 | commit, 294 | tree, 295 | tag, 296 | }; 297 | 298 | // pub fn expandPackDelta(allocator: mem.Allocator, reader: anytype, writer: anytype) !void { 299 | 300 | // } 301 | -------------------------------------------------------------------------------- /src/pack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const pack_index_zig = @import("pack_index.zig"); 9 | const helpers_zig = @import("helpers.zig"); 10 | const parseVariableLength = helpers_zig.parseVariableLength; 11 | 12 | 13 | pub fn packObjectReader(allocator: mem.Allocator, git_dir_path: []const u8, object_name: [20]u8) !*PackObjectReader { 14 | const search_result = try pack_index_zig.searchPackIndicies(allocator, git_dir_path, object_name); 15 | return try readObjectFromPack(allocator, git_dir_path, search_result.pack, search_result.offset); 16 | } 17 | 18 | pub fn readObjectFromPack(allocator: mem.Allocator, git_dir_path: []const u8, pack_name: [20]u8, offset: u64) !*PackObjectReader { 19 | const pack_file_name = try std.fmt.allocPrint(allocator, "pack-{s}.pack", .{ std.fmt.fmtSliceHexLower(&pack_name) }); 20 | defer allocator.free(pack_file_name); 21 | 22 | const pack_file_path = try fs.path.join(allocator, &.{ git_dir_path, "objects", "pack", pack_file_name }); 23 | defer allocator.free(pack_file_path); 24 | 25 | var pack = try Pack.init(allocator, pack_file_path); 26 | errdefer pack.deinit(); 27 | 28 | return PackObjectReader.init(allocator, pack, offset); 29 | } 30 | 31 | pub const PackObjectReader = struct { 32 | allocator: mem.Allocator, 33 | pack: *Pack, 34 | object_reader: ObjectReader, 35 | 36 | const Reader = ObjectReader.Reader; 37 | 38 | pub fn init(allocator: mem.Allocator, pack: *Pack, offset: usize) !*PackObjectReader { 39 | var pack_object_reader = try allocator.create(PackObjectReader); 40 | pack_object_reader.* = PackObjectReader{ 41 | .allocator = allocator, 42 | .pack = pack, 43 | .object_reader = try pack.readObjectAt(offset), 44 | }; 45 | return pack_object_reader; 46 | } 47 | 48 | pub fn reader(self: *PackObjectReader) Reader { 49 | return self.object_reader.reader(); 50 | } 51 | 52 | pub fn setOffset(self: *PackObjectReader, offset: usize) !void { 53 | // This could lead to bad state 54 | self.object_reader.deinit(); 55 | self.object_reader = try self.pack.readObjectAt(offset); 56 | } 57 | 58 | pub fn deinit(self: *PackObjectReader) void { 59 | self.object_reader.deinit(); 60 | self.pack.deinit(); 61 | self.allocator.destroy(self); 62 | } 63 | }; 64 | 65 | pub const Pack = struct { 66 | allocator: mem.Allocator, 67 | file: fs.File, 68 | header: Header, 69 | 70 | pub fn init(allocator: mem.Allocator, path: []const u8) !*Pack { 71 | const file = try fs.cwd().openFile(path, .{}); 72 | try file.seekTo(0); 73 | const reader = file.reader(); 74 | const signature = try reader.readBytesNoEof(4); 75 | const version = try reader.readIntBig(u32); 76 | const number_objects = try reader.readIntBig(u32); 77 | 78 | if (!mem.eql(u8, &signature, "PACK")) { 79 | return error.UnsupportedPackFile; 80 | } 81 | 82 | if (version != 2) { 83 | return error.UnsupportedPackVersion; 84 | } 85 | 86 | var pack = try allocator.create(Pack); 87 | pack.* = Pack { 88 | .allocator = allocator, 89 | .file = file, 90 | .header = Header { 91 | .signature = signature, 92 | .version = version, 93 | .number_objects = number_objects, 94 | }, 95 | }; 96 | 97 | return pack; 98 | } 99 | 100 | pub fn deinit(self: *Pack) void { 101 | self.file.close(); 102 | self.allocator.destroy(self); 103 | } 104 | 105 | pub fn format(self: Pack, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 106 | _ = fmt; 107 | _ = options; 108 | 109 | try out_stream.print("Pack{{ signature = {s}, version = {d}, number_objects = {d} }}", .{ self.header.signature, self.header.version, self.header.number_objects }); 110 | } 111 | 112 | pub fn readObjectAt(self: Pack, offset: usize) !ObjectReader { 113 | try self.file.seekTo(offset); 114 | const reader = self.file.reader(); 115 | const object_header = try parseObjectHeader(reader); 116 | 117 | var decompressor = try std.compress.zlib.decompressStream(self.allocator, reader); 118 | errdefer decompressor.deinit(); 119 | 120 | return ObjectReader{ 121 | .decompressor = decompressor, 122 | .header = object_header, 123 | .offset = offset, 124 | }; 125 | } 126 | 127 | pub fn iterator(self: *Pack) !ObjectIterator { 128 | return try ObjectIterator.init(self); 129 | } 130 | 131 | pub fn validate(self: *Pack) !void { 132 | try self.file.seekTo(0); 133 | const reader = self.file.reader(); 134 | 135 | var hasher = std.crypto.hash.Sha1.init(.{}); 136 | const writer = hasher.writer(); 137 | 138 | const file_length = try self.file.getEndPos(); 139 | const length_without_hash = file_length - 20; 140 | 141 | var bytes_written: usize = 0; 142 | var buffer: [4096]u8 = undefined; 143 | while (bytes_written < length_without_hash) { 144 | const bytes_remaining = length_without_hash - bytes_written; 145 | const to_read = if (bytes_remaining > 4096) 4096 else bytes_remaining; 146 | const bytes_read = try reader.read(buffer[0..to_read]); 147 | try writer.writeAll(buffer[0..bytes_read]); 148 | bytes_written += bytes_read; 149 | } 150 | const hash = hasher.finalResult(); 151 | const footer_hash = try reader.readBytesNoEof(20); 152 | if (!mem.eql(u8, &hash, &footer_hash)) { 153 | return error.HashMismatch; 154 | } 155 | } 156 | }; 157 | 158 | pub fn parseObjectHeader(reader: anytype) !ObjectHeader { 159 | var size: usize = 0; 160 | const first_byte = try reader.readByte(); 161 | const first_bits: ObjectFirstBit = @bitCast(first_byte); 162 | const object_type = first_bits.type; 163 | size += first_bits.size; 164 | var more: bool = first_bits.more; 165 | var shifts: u6 = 4; 166 | while (more) { 167 | const byte = try reader.readByte(); 168 | more = if (byte >> 7 == 0) false else true; 169 | const more_size_bits: u64 = byte & (0xFF >> 1); 170 | size += (more_size_bits << shifts); 171 | shifts += 7; 172 | } 173 | 174 | var delta_header: ?ObjectHeader.Delta = null; 175 | if (object_type == .ofs_delta) { 176 | delta_header = ObjectHeader.Delta{ .offset = try parseVariableLength(reader) }; 177 | } 178 | if (object_type == .ref_delta) { 179 | delta_header = ObjectHeader.Delta{ .ref = try reader.readBytesNoEof(20) }; 180 | } 181 | 182 | return ObjectHeader{ 183 | .size = size, 184 | .type = object_type, 185 | .delta = delta_header, 186 | }; 187 | } 188 | 189 | const ObjectFirstBit = packed struct(u8) { 190 | size: u4, 191 | type: PackObjectType, 192 | more: bool, 193 | }; 194 | 195 | 196 | 197 | pub const ObjectIterator = struct { 198 | pack: *Pack, 199 | current_object_reader: ?ObjectReader = null, 200 | current_end_pos: usize, 201 | 202 | pub fn init(pack: *Pack) !ObjectIterator { 203 | // Reset to right after the header 204 | return ObjectIterator{ 205 | .pack = pack, 206 | .current_end_pos = 12, 207 | }; 208 | } 209 | 210 | pub fn next(self: *ObjectIterator) !?Entry { 211 | // Finish existing decompressor so we're at the end of the 212 | // current object 213 | if (self.current_object_reader) |*existing_reader| { 214 | existing_reader.deinit(); 215 | } 216 | self.current_object_reader = null; 217 | 218 | // Account for the 20 byte pack sha1 checksum trailer 219 | if (self.current_end_pos + 20 == try self.pack.file.getEndPos()) { 220 | return null; 221 | } 222 | 223 | // Create new decompressor at current position, hash object 224 | // contents, reset decompressor and file position, pass new 225 | // decompressor to caller 226 | 227 | const object_begin = self.current_end_pos; 228 | 229 | var object_reader_hash = try self.pack.readObjectAt(object_begin); 230 | defer object_reader_hash.deinit(); 231 | 232 | var hasher = std.crypto.hash.Sha1.init(.{}); 233 | const hasher_writer = hasher.writer(); 234 | var counting_writer = std.io.countingWriter(hasher_writer); 235 | 236 | // Create object header just like in loose object file. 237 | // These don't work for delta objects. 238 | try hasher_writer.print("{s} {d}\x00", .{ @tagName(object_reader_hash.header.type), object_reader_hash.header.size }); 239 | 240 | var pump = std.fifo.LinearFifo(u8, .{ .Static = 4094 }).init(); 241 | try pump.pump(object_reader_hash.reader(), counting_writer.writer()); 242 | 243 | self.current_end_pos = try self.pack.file.getPos(); 244 | 245 | if (counting_writer.bytes_written != object_reader_hash.header.size) { 246 | return error.PackObjectSizeMismatch; 247 | } 248 | 249 | const object_name = hasher.finalResult(); 250 | 251 | // Create fresh reader for caller 252 | self.current_object_reader = try self.pack.readObjectAt(object_begin); 253 | 254 | return Entry{ 255 | .offset = object_begin, 256 | .object_name = object_name, 257 | .object_reader = &self.current_object_reader.?, 258 | }; 259 | } 260 | 261 | pub fn reset(self: *ObjectIterator) !void { 262 | try self.pack.file.seekTo(12); 263 | } 264 | 265 | pub fn deinit(self: *ObjectIterator) void { 266 | if (self.current_object_reader) |*current| { 267 | current.deinit(); 268 | } 269 | } 270 | 271 | pub const Entry = struct { 272 | offset: usize, 273 | object_name: [20]u8, 274 | object_reader: *ObjectReader, 275 | }; 276 | }; 277 | 278 | pub const Header = struct { 279 | signature: [4]u8, 280 | version: u32, 281 | number_objects: u32, 282 | }; 283 | 284 | pub const PackObjectType = enum(u3) { 285 | commit = 1, 286 | tree = 2, 287 | blob = 3, 288 | tag = 4, 289 | ofs_delta = 6, 290 | ref_delta = 7, 291 | }; 292 | 293 | pub const ObjectReader = struct { 294 | decompressor: Decompressor, 295 | header: ObjectHeader, 296 | offset: u64, 297 | 298 | pub const Decompressor = std.compress.zlib.DecompressStream(fs.File.Reader); 299 | pub const Reader = Decompressor.Reader; 300 | const Self = @This(); 301 | 302 | pub fn reader(self: *ObjectReader) Reader { 303 | return self.decompressor.reader(); 304 | } 305 | 306 | pub fn deinit(self: *ObjectReader) void { 307 | self.decompressor.deinit(); 308 | } 309 | }; 310 | 311 | 312 | pub const ObjectHeader = struct { 313 | size: usize, 314 | type: PackObjectType, 315 | delta: ?Delta, 316 | 317 | const Delta = union(enum) { 318 | offset: usize, 319 | ref: [20]u8, 320 | }; 321 | }; 322 | -------------------------------------------------------------------------------- /src/pack_index.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const helpers = @import("helpers.zig"); 9 | 10 | pub fn searchPackIndicies(allocator: mem.Allocator, git_dir_path: []const u8, object_name: [20]u8) !PackIndexResult { 11 | const pack_dir_path = try fs.path.join(allocator, &.{ git_dir_path, "objects", "pack" }); 12 | defer allocator.free(pack_dir_path); 13 | 14 | const pack_dir = try fs.cwd().openIterableDir(pack_dir_path, .{}); 15 | var pack_dir_iter = pack_dir.iterate(); 16 | while (try pack_dir_iter.next()) |dir_entry| { 17 | if (mem.endsWith(u8, dir_entry.name, ".idx")) { 18 | const pack_index_path = try fs.path.join(allocator, &.{ pack_dir_path, dir_entry.name }); 19 | defer allocator.free(pack_index_path); 20 | 21 | var pack_index = try PackIndex.init(pack_index_path); 22 | defer pack_index.deinit(); 23 | 24 | // Remove `pack-` and `.idx` 25 | const pack_name = try helpers.hexDigestToObjectName(dir_entry.name[5..45]); 26 | 27 | if (try pack_index.find(object_name)) |offset| { 28 | return PackIndexResult{ 29 | .pack = pack_name, 30 | .offset = offset, 31 | }; 32 | } 33 | } 34 | } 35 | 36 | return error.ObjectNotFound; 37 | } 38 | 39 | pub const PackIndexResult = struct { 40 | pack: [20]u8, 41 | offset: u64, 42 | }; 43 | 44 | pub const PackIndex = struct { 45 | file: fs.File, 46 | version: u32, 47 | fanout_table: [256]u32, 48 | 49 | pub fn init(path: []const u8) !PackIndex { 50 | const file = try fs.cwd().openFile(path, .{}); 51 | try file.seekTo(0); 52 | const reader = file.reader(); 53 | var fanout: [256]u32 = undefined; 54 | var version: u32 = 0; 55 | 56 | const magic_number = try reader.readBytesNoEof(4); 57 | if (mem.eql(u8, &magic_number, "\xfftOc")) { 58 | version = try reader.readIntBig(u32); 59 | fanout[0] = try reader.readIntBig(u32); 60 | } else { 61 | version = 1; 62 | fanout[0] = std.mem.readIntBig(u32, &magic_number); 63 | } 64 | 65 | for (1..256) |i| { 66 | fanout[i] = try reader.readIntBig(u32); 67 | } 68 | 69 | return PackIndex{ 70 | .file = file, 71 | .version = version, 72 | .fanout_table = fanout, 73 | }; 74 | } 75 | 76 | pub fn deinit(self: PackIndex) void { 77 | self.file.close(); 78 | } 79 | 80 | pub fn find(self: PackIndex, object_name: [20]u8) !?usize { 81 | if (self.version == 1) { 82 | // TODO index version 1 lookup 83 | return error.Unimplemented; 84 | } 85 | if (try self.version2FindObjectIndex(object_name)) |index| { 86 | return try self.version2OffsetAtIndex(index); 87 | } 88 | return null; 89 | } 90 | 91 | pub fn version2FindObjectIndex(self: PackIndex, object_name: [20]u8) !?usize { 92 | var bottom: u32 = 0; 93 | var top: u32 = self.fanout_table[object_name[0]]; 94 | while (top > bottom) { 95 | const halfway: u32 = ((top - bottom) / 2) + bottom; 96 | const halfway_object_name = try self.version2ObjectNameAtIndex(halfway); 97 | if (mem.eql(u8, &object_name, &halfway_object_name)) { 98 | return halfway; 99 | } 100 | if (mem.lessThan(u8, &object_name, &halfway_object_name)) { 101 | if (halfway == 0) 102 | break; 103 | top = halfway - 1; 104 | } else { 105 | if (bottom == self.fanout_table[object_name[0]]) 106 | break; 107 | bottom = halfway + 1; 108 | } 109 | } 110 | const last_object_name = try self.version2ObjectNameAtIndex(top); 111 | if (mem.eql(u8, &object_name, &last_object_name)) { 112 | return top; 113 | } 114 | return null; 115 | } 116 | 117 | pub fn version2ObjectNameAtIndex(self: PackIndex, index: usize) ![20]u8 { 118 | const num_entries = self.fanout_table[255]; 119 | if (index > num_entries) { 120 | return error.IndexOutOfBounds; 121 | } 122 | const end_of_fanout = 4 + 4 + (256 * 4); 123 | const index_position = end_of_fanout + (index * 20); 124 | try self.file.seekTo(index_position); 125 | return try self.file.reader().readBytesNoEof(20); 126 | } 127 | 128 | pub fn version2OffsetAtIndex(self: PackIndex, index: usize) !u64 { 129 | const total_entries = self.fanout_table[255]; 130 | if (index > total_entries) { 131 | return error.IndexOutOfBounds; 132 | } 133 | const reader = self.file.reader(); 134 | const end_of_fanout = 4 + 4 + (256 * 4); 135 | const end_of_object_names = end_of_fanout + (total_entries * 20); 136 | const end_of_crc = end_of_object_names + (total_entries * 4); 137 | const end_of_offset32 = end_of_crc + (total_entries * 4); 138 | 139 | const index32_pos = end_of_crc + (4 * index); 140 | try self.file.seekTo(index32_pos); 141 | const index32 = try reader.readIntBig(u32); 142 | const index32_value = index32 & (std.math.maxInt(u32) >> 1); 143 | if (index32 >> 31 == 0) { 144 | return index32_value; 145 | } 146 | 147 | const index64_pos = end_of_offset32 + (index32_value * 8); 148 | try self.file.seekTo(index64_pos); 149 | return try reader.readIntBig(u64); 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /src/ref.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const helpers = @import("helpers.zig"); 9 | const lessThanStrings = helpers.lessThanStrings; 10 | const hexDigestToObjectName = helpers.hexDigestToObjectName; 11 | 12 | /// Turns user input into a Ref 13 | pub fn cannonicalizeRef(allocator: mem.Allocator, git_dir_path: []const u8, ref_or_object_name: []const u8) !?Ref { 14 | const expanded_ref = try expandRef(allocator, ref_or_object_name); 15 | defer allocator.free(expanded_ref); 16 | 17 | const resolved_ref = try readRef(allocator, git_dir_path, expanded_ref); 18 | defer { 19 | if (resolved_ref) |rr| 20 | rr.deinit(allocator); 21 | } 22 | if (resolved_ref != null) { 23 | return Ref{ .ref = try allocator.dupe(u8, expanded_ref) }; 24 | } 25 | const object_name = hexDigestToObjectName(ref_or_object_name) catch return null; 26 | return Ref{ .object_name = object_name }; 27 | } 28 | 29 | /// Recursively resolves a ref or object name hash. 30 | /// Useful for processing user input. 31 | pub fn resolveRefOrObjectName(allocator: mem.Allocator, git_dir_path: []const u8, ref_or_object_name: []const u8) !?[20]u8 { 32 | const resolved_ref = try resolveRef(allocator, git_dir_path, ref_or_object_name); 33 | if (resolved_ref) |ref| { 34 | return ref; 35 | } 36 | return hexDigestToObjectName(ref_or_object_name) catch null; 37 | } 38 | 39 | /// Recursively resolves the HEAD until an object name is found. 40 | pub fn resolveHead(allocator: mem.Allocator, git_dir_path: []const u8) !?[20]u8 { 41 | return try resolveRef(allocator, git_dir_path, "HEAD"); 42 | } 43 | 44 | /// Recursively resolves refs until an object name is found. 45 | pub fn resolveRef(allocator: mem.Allocator, git_dir_path: []const u8, ref: []const u8) !?[20]u8 { 46 | const current_ref = try readRef(allocator, git_dir_path, ref) orelse return null; 47 | 48 | switch (current_ref) { 49 | // TODO avoid infinite recursion on cyclical references 50 | .ref => |ref_name| { 51 | defer current_ref.deinit(allocator); 52 | return try resolveRef(allocator, git_dir_path, ref_name); 53 | }, 54 | .object_name => |object_name| return object_name, 55 | } 56 | } 57 | 58 | // FIXME This won't work on windows 59 | /// Returns the filesystem path to a ref 60 | /// Caller responsible for memory 61 | pub fn refToPath(allocator: mem.Allocator, git_dir_path: []const u8, ref: []const u8) ![]const u8 { 62 | const full_ref = try expandRef(allocator, ref); 63 | defer allocator.free(full_ref); 64 | 65 | return fs.path.join(allocator, &.{ git_dir_path, full_ref }); 66 | } 67 | 68 | /// Returns the full expanded name of a ref 69 | /// Caller responsible for memory 70 | pub fn expandRef(allocator: mem.Allocator, ref: []const u8) ![]const u8 { 71 | if (mem.eql(u8, ref, "HEAD") or mem.startsWith(u8, ref, "refs/")) { 72 | return allocator.dupe(u8, ref); 73 | } else if (mem.indexOf(u8, ref, "/") == null) { 74 | // FIXME This won't work on windows 75 | return fs.path.join(allocator, &.{ "refs/heads", ref }); 76 | } 77 | return error.InvalidRef; 78 | } 79 | 80 | 81 | /// Updates the target for HEAD 82 | pub fn updateHead(allocator: mem.Allocator, git_dir_path: []const u8, target: Ref) !void { 83 | try updateRef(allocator, git_dir_path, "HEAD", target); 84 | } 85 | 86 | /// Updates the target for a ref 87 | pub fn updateRef(allocator: mem.Allocator, git_dir_path: []const u8, ref: []const u8, target: Ref) !void { 88 | const full_path = try refToPath(allocator, git_dir_path, ref); 89 | defer allocator.free(full_path); 90 | 91 | const file = try fs.cwd().createFile(full_path, .{}); 92 | defer file.close(); 93 | 94 | switch (target) { 95 | .ref => |ref_name| { 96 | const full_ref = try expandRef(allocator, ref_name); 97 | defer allocator.free(full_ref); 98 | 99 | try file.writer().print("ref: {s}\n", .{ full_ref }); 100 | }, 101 | .object_name => |object_name| try file.writer().print("{s}\n", .{ std.fmt.fmtSliceHexLower(&object_name) }), 102 | } 103 | } 104 | 105 | /// Represents a git ref. Either an object name or a pointer to 106 | /// another ref 107 | pub const Ref = union(enum) { 108 | ref: []const u8, 109 | object_name: [20]u8, 110 | 111 | pub fn deinit(self: Ref, allocator: mem.Allocator) void { 112 | switch (self) { 113 | .ref => allocator.free(self.ref), 114 | else => {}, 115 | } 116 | } 117 | 118 | pub fn format(self: Ref, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 119 | _ = fmt; 120 | _ = options; 121 | 122 | switch (self) { 123 | .ref => |ref| try out_stream.print("{s}", .{ ref }), 124 | .object_name => |object_name| try out_stream.print("{s}", .{ std.fmt.fmtSliceHexLower(&object_name) }), 125 | } 126 | } 127 | }; 128 | 129 | // XXX Still not sure if an optional is the right interface here, 130 | // maybe it should just return an error in a ref doesn't exist. 131 | /// Returns the target of a ref 132 | pub fn readRef(allocator: mem.Allocator, git_dir_path: []const u8, ref: []const u8) !?Ref { 133 | const ref_path = try refToPath(allocator, git_dir_path, ref); 134 | defer allocator.free(ref_path); 135 | 136 | const file = fs.cwd().openFile(ref_path, .{}) catch |err| switch (err) { 137 | error.FileNotFound => return null, 138 | else => return err, 139 | }; 140 | defer file.close(); 141 | 142 | const data = try file.reader().readUntilDelimiterAlloc(allocator, '\n', 4096); 143 | defer allocator.free(data); 144 | 145 | if (mem.startsWith(u8, data, "ref: ")) { 146 | return .{ .ref = try allocator.dupe(u8, data[5..]) }; 147 | } else { 148 | return .{ .object_name = try hexDigestToObjectName(data[0..40]) }; 149 | } 150 | } 151 | 152 | /// Returns the current HEAD Ref 153 | pub fn currentHead(allocator: mem.Allocator, git_dir_path: []const u8) !?Ref { 154 | return readRef(allocator, git_dir_path, "HEAD"); 155 | } 156 | 157 | /// Returns the full current HEAD ref, only if it's the name of another 158 | /// ref. Retruns null if it's anything else 159 | /// Caller responsible for memory. 160 | pub fn currentRef(allocator: mem.Allocator, git_dir_path: []const u8) !?[]const u8 { 161 | const current_ref = try readRef(allocator, git_dir_path, "HEAD") orelse return null; 162 | return switch (current_ref) { 163 | .ref => |ref| ref, 164 | .object_name => null, 165 | }; 166 | } 167 | 168 | /// Returns the name of the current head ref (branch name) 169 | pub fn currentHeadRef(allocator: mem.Allocator, git_dir_path: []const u8) !?[]const u8 { 170 | const current_ref = try currentRef(allocator, git_dir_path) orelse return null; 171 | 172 | defer allocator.free(current_ref); 173 | 174 | const current_head_ref = std.mem.trimLeft(u8, current_ref, "refs/heads/"); 175 | return try allocator.dupe(u8, current_head_ref); 176 | } 177 | 178 | /// Returns a list of all head ref names (branch names) 179 | pub fn listHeadRefs(allocator: mem.Allocator, git_dir_path: []const u8) !RefList { 180 | const refs_path = try fs.path.join(allocator, &.{ git_dir_path, "refs", "heads" }); 181 | defer allocator.free(refs_path); 182 | const refs_dir = try fs.cwd().openIterableDir(refs_path, .{}); 183 | var iter = refs_dir.iterate(); 184 | var head_list = std.ArrayList([]const u8).init(allocator); 185 | while (try iter.next()) |dir| { 186 | try head_list.append(try allocator.dupe(u8, dir.name)); 187 | } 188 | 189 | return .{ 190 | .allocator = allocator, 191 | .refs = try head_list.toOwnedSlice(), 192 | }; 193 | } 194 | 195 | /// Returns a list of all ref names, including heads, remotes, and tags 196 | pub fn listRefs(allocator: mem.Allocator, git_dir_path: []const u8) !RefList { 197 | const refs_path = try fs.path.join(allocator, &.{ git_dir_path, "refs" }); 198 | defer allocator.free(refs_path); 199 | 200 | var iter = try fs.cwd().openIterableDir(refs_path, .{}); 201 | defer iter.close(); 202 | 203 | var walker = try iter.walk(allocator); 204 | defer walker.deinit(); 205 | 206 | var ref_list = std.ArrayList([]const u8).init(allocator); 207 | 208 | while (try walker.next()) |walker_entry| { 209 | switch (walker_entry.kind) { 210 | .file => { 211 | const ref_path = try fs.path.join(allocator, &.{ "refs", walker_entry.path }); 212 | try ref_list.append(ref_path); 213 | }, 214 | else => continue, 215 | } 216 | } 217 | 218 | var sorted_ref_list = try ref_list.toOwnedSlice(); 219 | mem.sort([]const u8, sorted_ref_list, {}, lessThanStrings); 220 | 221 | return .{ 222 | .allocator = allocator, 223 | .refs = sorted_ref_list, 224 | }; 225 | } 226 | 227 | /// A list of ref names (strings) 228 | pub const RefList = struct { 229 | allocator: mem.Allocator, 230 | refs: [][]const u8, 231 | 232 | pub fn deinit(self: RefList) void { 233 | for (self.refs) |ref| { 234 | self.allocator.free(ref); 235 | } 236 | self.allocator.free(self.refs); 237 | } 238 | }; 239 | -------------------------------------------------------------------------------- /src/status.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | const os = std.os; 4 | const mem = std.mem; 5 | const debug = std.debug; 6 | const testing = std.testing; 7 | 8 | const helpers = @import("helpers.zig"); 9 | const index_zig = @import("index.zig"); 10 | const IndexEntry = index_zig.Index.Entry; 11 | const readIndex = index_zig.readIndex; 12 | const object_zig = @import("object.zig"); 13 | const repoToGitDir = helpers.repoToGitDir; 14 | const tree_zig = @import("tree.zig"); 15 | const ref_zig = @import("ref.zig"); 16 | const commit_zig = @import("commit.zig"); 17 | 18 | const saveObject = @import("object.zig").saveObject; 19 | 20 | /// Compare the working area to the index file and return the results. 21 | pub fn repoStatus(allocator: mem.Allocator, repo_path: []const u8) !*StatusDiff { 22 | const git_dir_path = try repoToGitDir(allocator, repo_path); 23 | defer allocator.free(git_dir_path); 24 | 25 | const index = try readIndex(allocator, repo_path); 26 | defer index.deinit(); 27 | 28 | var status_diff = try StatusDiff.init(allocator); 29 | errdefer status_diff.deinit(); 30 | 31 | var path_buffer: [fs.MAX_PATH_BYTES]u8 = undefined; 32 | 33 | // Checking for removed, modified, or unmodified files 34 | for (index.entries.items) |entry| { 35 | if (entry.mode.object_type != .regular_file) { 36 | // TODO Is it possible to check for gitlink and symlink updates? 37 | continue; 38 | } 39 | var path_allocator = std.heap.FixedBufferAllocator.init(&path_buffer); 40 | const full_path = try fs.path.join(path_allocator.allocator(), &.{ repo_path, entry.path }); 41 | const file = fs.cwd().openFile(full_path, .{}) catch |err| switch (err) { 42 | error.FileNotFound => { 43 | try status_diff.entries.append(.{ .path = try allocator.dupe(u8, entry.path), .status = .removed, .object_name = entry.object_name }); 44 | continue; 45 | }, 46 | else => return err, 47 | }; 48 | defer file.close(); 49 | 50 | // If the file metadata isn't changed, assume the file isn't 51 | // for speed 52 | if (!try fileStatChangedFromEntry(file, entry)) { 53 | continue; 54 | } 55 | const stat = try file.stat(); 56 | const file_hash = try object_zig.hashFile(file); 57 | if (!mem.eql(u8, &file_hash, &entry.object_name) or stat.mode != @as(u32, @bitCast(entry.mode))) { 58 | try status_diff.entries.append(.{ .path = try allocator.dupe(u8, entry.path), .status = .modified, .object_name = file_hash }); 59 | } 60 | } 61 | 62 | // Checking for untracked files 63 | var all_path_set = std.BufSet.init(allocator); 64 | defer all_path_set.deinit(); 65 | 66 | var index_path_set = std.StringHashMap(ObjectDetails).init(allocator); 67 | defer index_path_set.deinit(); 68 | 69 | var dir_iterable = try fs.cwd().openIterableDir(repo_path, .{}); 70 | var walker = try dir_iterable.walk(allocator); 71 | defer walker.deinit(); 72 | 73 | while (try walker.next()) |dir_entry| { 74 | switch (dir_entry.kind) { 75 | .sym_link, .file => { 76 | if (mem.startsWith(u8, dir_entry.path, ".git") or mem.indexOf(u8, dir_entry.path, fs.path.sep_str ++ ".git") != null) { 77 | continue; 78 | } 79 | try all_path_set.insert(dir_entry.path); 80 | }, 81 | else => continue, 82 | } 83 | } 84 | 85 | for (index.entries.items) |entry| { 86 | try index_path_set.put(entry.path, .{ .object_name = entry.object_name, .mode = entry.mode }); 87 | } 88 | 89 | var all_path_iter = all_path_set.iterator(); 90 | while (all_path_iter.next()) |all_entry| { 91 | if (index_path_set.get(all_entry.*) == null) { 92 | try status_diff.entries.append(.{ .path = try allocator.dupe(u8, all_entry.*), .status = .untracked, .object_name = null }); 93 | } 94 | } 95 | 96 | // Checking for files that are staged but not committed 97 | // This includes deleted, modified, new files (oh no) 98 | if (try ref_zig.resolveHead(allocator, git_dir_path)) |commit_object_name| { 99 | const commit = try commit_zig.readCommit(allocator, git_dir_path, commit_object_name); 100 | defer commit.deinit(); 101 | 102 | const tree_object_name = commit.tree; 103 | var tree_walker = try tree_zig.walkTree(allocator, git_dir_path, tree_object_name); 104 | defer tree_walker.deinit(); 105 | 106 | var tree_path_set = std.StringHashMap(ObjectDetails).init(allocator); 107 | defer { 108 | var iter = tree_path_set.iterator(); 109 | while (iter.next()) |iter_entry| { 110 | allocator.free(iter_entry.key_ptr.*); 111 | } 112 | tree_path_set.deinit(); 113 | } 114 | 115 | while (try tree_walker.next()) |tree_entry| { 116 | try tree_path_set.put(try allocator.dupe(u8, tree_entry.path), .{ .object_name = tree_entry.object_name, .mode = tree_entry.mode }); 117 | if (index_path_set.get(tree_entry.path)) |index_entry| { 118 | // In index and tree 119 | if (!mem.eql(u8, &index_entry.object_name, &tree_entry.object_name)) { 120 | // Object names don't match 121 | try status_diff.entries.append(.{ .path = try allocator.dupe(u8, tree_entry.path), .status = .staged_modified, .object_name = tree_entry.object_name }); 122 | } 123 | } else { 124 | // In tree, not in index 125 | // TODO How to deal with staged removal of non-regular files 126 | if (tree_entry.mode.object_type != .regular_file) { 127 | continue; 128 | } 129 | try status_diff.entries.append(.{ .path = try allocator.dupe(u8, tree_entry.path), .status = .staged_removed, .object_name = tree_entry.object_name }); 130 | } 131 | } 132 | 133 | var index_iter = index_path_set.iterator(); 134 | while (index_iter.next()) |index_entry| { 135 | if (tree_path_set.contains(index_entry.key_ptr.*)) { 136 | continue; 137 | } 138 | // In index, not in tree 139 | try status_diff.entries.append(.{ .path = try allocator.dupe(u8, index_entry.key_ptr.*), .status = .staged_added, .object_name = index_entry.value_ptr.object_name }); 140 | } 141 | } 142 | 143 | mem.sort(StatusDiff.Entry, status_diff.entries.items, {}, StatusDiff.Entry.lessThan); 144 | 145 | return status_diff; 146 | } 147 | 148 | const ObjectDetails = struct { object_name: [20]u8, mode: index_zig.Index.Mode }; 149 | 150 | pub fn fileStatChangedFromEntry(file: fs.File, entry: IndexEntry) !bool { 151 | const stat = try os.fstat(file.handle); 152 | const ctime = stat.ctime(); 153 | const mtime = stat.mtime(); 154 | if (ctime.tv_sec != entry.ctime_s) return true; 155 | if (ctime.tv_nsec != entry.ctime_n) return true; 156 | if (mtime.tv_sec != entry.mtime_s) return true; 157 | if (mtime.tv_nsec != entry.mtime_n) return true; 158 | if (stat.ino != entry.ino) return true; 159 | if (stat.dev != entry.dev) return true; 160 | if (stat.mode != @as(u32, @bitCast(entry.mode))) return true; 161 | if (stat.size != entry.file_size) return true; 162 | return false; 163 | } 164 | 165 | pub const StatusDiff = struct { 166 | entries: EntryList, 167 | 168 | pub fn init(allocator: mem.Allocator) !*StatusDiff { 169 | var status_diff = try allocator.create(StatusDiff); 170 | status_diff.entries = EntryList.init(allocator); 171 | return status_diff; 172 | } 173 | 174 | pub fn deinit(self: *const StatusDiff) void { 175 | for (self.entries.items) |entry| { 176 | self.entries.allocator.free(entry.path); 177 | } 178 | self.entries.deinit(); 179 | self.entries.allocator.destroy(self); 180 | } 181 | 182 | const EntryList = std.ArrayList(Entry); 183 | 184 | pub const Entry = struct { 185 | path: []const u8, 186 | status: Status, 187 | object_name: ?[20]u8, 188 | 189 | pub fn lessThan(ctx: void, a: Entry, b: Entry) bool { 190 | _ = ctx; 191 | return mem.lessThan(u8, a.path, b.path); 192 | } 193 | }; 194 | 195 | pub const Status = enum { 196 | /// The file is modified and staged, but not yet committed 197 | staged_modified, 198 | /// The file is removed from the index, but not yet committed 199 | staged_removed, 200 | /// The file is added to the index, but not yet committed 201 | staged_added, 202 | /// The file is not tracked by the index 203 | untracked, 204 | /// The file has not been modified compared to the index 205 | unmodified, 206 | /// The file is different from the version tracked by the index 207 | modified, 208 | /// The file is listed in the index, but does not exist on disk 209 | removed, 210 | }; 211 | }; 212 | -------------------------------------------------------------------------------- /src/tag.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const object_zig = @import("object.zig"); 9 | const loadObject = object_zig.loadObject; 10 | const ObjectType = object_zig.ObjectType; 11 | 12 | const Commit = @import("commit.zig").Commit; 13 | 14 | const helpers = @import("helpers.zig"); 15 | const hexDigestToObjectName = helpers.hexDigestToObjectName; 16 | 17 | /// Returns a Tag with a certain name 18 | pub fn readTag(allocator: mem.Allocator, git_dir_path: []const u8, tag_object_name: [20]u8) !Tag { 19 | var tag_data = std.ArrayList(u8).init(allocator); 20 | defer tag_data.deinit(); 21 | 22 | const object_header = try loadObject(allocator, git_dir_path, tag_object_name, tag_data.writer()); 23 | if (object_header.type != .tag) { 24 | return error.IncorrectObjectType; 25 | } 26 | 27 | var object_name: ?[20]u8 = null; 28 | var tag_type: ?ObjectType = null; 29 | var tag_tag: ?[]const u8 = null; 30 | var tagger: ?Commit.Committer = null; 31 | 32 | var lines = mem.split(u8, tag_data.items, "\n"); 33 | while (lines.next()) |line| { 34 | if (mem.eql(u8, line, "")) { 35 | break; 36 | } 37 | var words = mem.tokenize(u8, line, " "); 38 | const key = words.next() orelse return error.InvalidTagProperty; 39 | 40 | if (mem.eql(u8, key, "object")) { 41 | const hex = words.next() orelse return error.InvalidObjectName; 42 | object_name = try hexDigestToObjectName(hex); 43 | } else if (mem.eql(u8, key, "type")) { 44 | const obj_type = words.next() orelse return error.InvalidObjectType; 45 | tag_type = std.meta.stringToEnum(ObjectType, obj_type) orelse return error.InvalidObjectType; 46 | } else if (mem.eql(u8, key, "tag")) { 47 | const tag_name = words.next() orelse return error.InvalidTagName; 48 | tag_tag = try allocator.dupe(u8, tag_name); 49 | } else if (mem.eql(u8, key, "tagger")) { 50 | const tag_tagger = words.rest(); 51 | tagger = try Commit.Committer.parse(allocator, tag_tagger); 52 | } 53 | } 54 | 55 | const message = try allocator.dupe(u8, lines.rest()); 56 | 57 | return Tag{ 58 | .allocator = allocator, 59 | .object_name = object_name orelse return error.InvalidObjectName, 60 | .type = tag_type orelse return error.InvalidObjectType, 61 | .tag = tag_tag orelse return error.InvalidTagName, 62 | .tagger = tagger orelse return error.InvalidTagger, 63 | .message = message, 64 | }; 65 | } 66 | 67 | // TODO writeTag 68 | 69 | pub const Tag = struct { 70 | allocator: mem.Allocator, 71 | object_name: [20]u8, 72 | type: ObjectType, 73 | tag: []const u8, 74 | tagger: Commit.Committer, 75 | message: []const u8, 76 | 77 | pub fn deinit(self: Tag) void { 78 | self.allocator.free(self.tag); 79 | self.tagger.deinit(self.allocator); 80 | self.allocator.free(self.message); 81 | } 82 | 83 | pub fn format(self: Tag, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 84 | _ = fmt; 85 | _ = options; 86 | try out_stream.print("Tag{{ object_name: {s}, type: {s}, tag: {s}, tagger: {}, message: \"{s}\" }}", .{ std.fmt.fmtSliceHexLower(&self.object_name), @tagName(self.type), self.tag, self.tagger, self.message }); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /src/tree.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fs = std.fs; 4 | const mem = std.mem; 5 | const os = std.os; 6 | const testing = std.testing; 7 | 8 | const object_zig = @import("object.zig"); 9 | const saveObject = object_zig.saveObject; 10 | const loadObject = object_zig.loadObject; 11 | 12 | const index_zig = @import("index.zig"); 13 | const Index = index_zig.Index; 14 | const IndexList = index_zig.IndexList; 15 | const readIndex = index_zig.readIndex; 16 | const addFileToIndex = index_zig.addFileToIndex; 17 | 18 | const helpers = @import("helpers.zig"); 19 | const StringList = helpers.StringList; 20 | 21 | pub fn restoreTree(allocator: mem.Allocator, repo_path: []const u8, tree_object_name: [20]u8) !*Index { 22 | const git_dir_path = try helpers.repoToGitDir(allocator, repo_path); 23 | defer allocator.free(git_dir_path); 24 | 25 | var tree_iter = try walkTree(allocator, git_dir_path, tree_object_name); 26 | defer tree_iter.deinit(); 27 | 28 | var index = try Index.init(allocator); 29 | errdefer index.deinit(); 30 | 31 | var path_buffer: [fs.MAX_PATH_BYTES]u8 = undefined; 32 | 33 | while (try tree_iter.next()) |entry| { 34 | if (entry.mode.object_type != .regular_file) { 35 | // TODO Handle restoring other types of files, being more 36 | // efficient than just overwriting every file 37 | continue; 38 | } 39 | var path_allocator = std.heap.FixedBufferAllocator.init(&path_buffer); 40 | const entry_full_path = try fs.path.join(path_allocator.allocator(), &.{ repo_path, entry.path }); 41 | const object_name = entry.object_name; 42 | 43 | const entry_dir_path = fs.path.dirname(entry_full_path); 44 | if (entry_dir_path) |exists_path| { 45 | fs.cwd().access(exists_path, .{}) catch { 46 | try fs.cwd().makePath(exists_path); 47 | }; 48 | } 49 | 50 | const file = try fs.cwd().createFile(entry_full_path, .{ .mode = entry.mode.unix_permissions }); 51 | errdefer file.close(); 52 | 53 | // TODO We could check I suppose, maybe later 54 | _ = try object_zig.loadObject(allocator, git_dir_path, object_name, file.writer()); 55 | try file.sync(); 56 | file.close(); 57 | 58 | try addFileToIndex(allocator, repo_path, index, entry_full_path); 59 | 60 | std.debug.print("Restored File: [{s}] {s}, full_path: {s}\n", .{ std.fmt.fmtSliceHexLower(&entry.object_name), entry.path, entry_full_path }); 61 | } 62 | 63 | return index; 64 | } 65 | 66 | pub fn readTree(allocator: mem.Allocator, git_dir_path: []const u8, object_name: [20]u8) !Tree { 67 | var entries = std.ArrayList(Tree.Entry).init(allocator); 68 | var object = std.ArrayList(u8).init(allocator); 69 | defer object.deinit(); 70 | 71 | const object_type = try loadObject(allocator, git_dir_path, object_name, object.writer()); 72 | if (object_type.type != .tree) { 73 | return error.IncorrectObjectType; 74 | } 75 | 76 | var buffer = std.io.fixedBufferStream(object.items); 77 | const object_reader = buffer.reader(); 78 | 79 | while (buffer.pos != object.items.len) { 80 | var mode_buffer: [16]u8 = undefined; 81 | const mode_text = try object_reader.readUntilDelimiter(&mode_buffer, ' '); 82 | const mode = @as(Index.Mode, @bitCast(try std.fmt.parseInt(u32, mode_text, 8))); 83 | const path = try object_reader.readUntilDelimiterAlloc(allocator, 0, fs.MAX_PATH_BYTES); 84 | const tree_object_name = try object_reader.readBytesNoEof(20); 85 | 86 | const entry = Tree.Entry{ 87 | .mode = mode, 88 | .path = path, 89 | .object_name = tree_object_name, 90 | }; 91 | 92 | try entries.append(entry); 93 | } 94 | 95 | return Tree{ 96 | .allocator = allocator, 97 | .entries = try entries.toOwnedSlice(), 98 | }; 99 | } 100 | 101 | /// Writes a tree to an object and returns the name 102 | pub fn writeTree(allocator: mem.Allocator, git_dir_path: []const u8, tree: Tree) ![20]u8 { 103 | var tree_data = std.ArrayList(u8).init(allocator); 104 | defer tree_data.deinit(); 105 | var tree_writer = tree_data.writer(); 106 | 107 | mem.sort(Tree.Entry, tree.entries, {}, sortTreeEntries); 108 | 109 | for (tree.entries) |entry| { 110 | try tree_writer.print("{o} {s}\x00", .{ @as(u32, @bitCast(entry.mode)), entry.path }); 111 | try tree_writer.writeAll(&entry.object_name); 112 | } 113 | 114 | return saveObject(allocator, git_dir_path, tree_data.items, .tree); 115 | } 116 | 117 | fn sortTreeEntries(context: void, lhs: Tree.Entry, rhs: Tree.Entry) bool { 118 | _ = context; 119 | return mem.lessThan(u8, lhs.path, rhs.path); 120 | } 121 | 122 | pub const TreeList = std.ArrayList(Tree); 123 | 124 | pub const Tree = struct { 125 | allocator: mem.Allocator, 126 | entries: []Entry, 127 | 128 | pub fn deinit(self: *const Tree) void { 129 | for (self.entries) |entry| { 130 | entry.deinit(self.allocator); 131 | } 132 | self.allocator.free(self.entries); 133 | } 134 | 135 | pub const Entry = struct { 136 | mode: Index.Mode, 137 | path: []const u8, 138 | object_name: [20]u8, 139 | 140 | pub fn deinit(self: Entry, allocator: mem.Allocator) void { 141 | allocator.free(self.path); 142 | } 143 | 144 | pub fn format(self: Entry, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype) !void { 145 | _ = options; 146 | _ = fmt; 147 | 148 | try out_stream.print("Tree.Entry{{ mode: {o: >6}, object_name: {s}, path: {s} }}", .{ @as(u32, @bitCast(self.mode)), std.fmt.fmtSliceHexLower(&self.object_name), self.path }); 149 | } 150 | }; 151 | 152 | pub const EntryList = std.ArrayList(Tree.Entry); 153 | }; 154 | 155 | /// Transforms an Index into a tree object and stores it. Returns the 156 | /// object's name 157 | pub fn indexToTree(child_allocator: mem.Allocator, repo_path: []const u8) ![20]u8 { 158 | var arena = std.heap.ArenaAllocator.init(child_allocator); 159 | defer arena.deinit(); 160 | const allocator = arena.allocator(); 161 | 162 | const index = try readIndex(allocator, repo_path); 163 | 164 | var root = try NestedTree.init(allocator, ""); 165 | 166 | for (index.entries.items) |index_entry| { 167 | var tree_entry = Tree.Entry{ 168 | .mode = index_entry.mode, 169 | .path = fs.path.basename(index_entry.path), 170 | .object_name = index_entry.object_name, 171 | }; 172 | 173 | const dir = fs.path.dirname(index_entry.path); 174 | 175 | if (dir == null) { 176 | try root.entries.append(tree_entry); 177 | continue; 178 | } 179 | 180 | const valid_dir = dir.?; 181 | var dir_iter = mem.split(u8, valid_dir, fs.path.sep_str); 182 | var cur_tree = &root; 183 | iter: while (dir_iter.next()) |sub_dir| { 184 | for (cur_tree.subtrees.items) |*subtree| { 185 | if (mem.eql(u8, subtree.path, sub_dir)) { 186 | cur_tree = subtree; 187 | continue :iter; 188 | } 189 | } 190 | var nested_tree = try NestedTree.init(allocator, sub_dir); 191 | try cur_tree.subtrees.append(nested_tree); 192 | cur_tree = &cur_tree.subtrees.items[cur_tree.subtrees.items.len-1]; 193 | } 194 | try cur_tree.entries.append(tree_entry); 195 | } 196 | 197 | const git_dir_path = try fs.path.join(allocator, &.{ repo_path, ".git" }); 198 | 199 | return root.toTree(git_dir_path); 200 | } 201 | 202 | /// Represents a nested git tree (trees are flat with references to 203 | /// other trees) 204 | const NestedTree = struct { 205 | allocator: mem.Allocator, 206 | entries: Tree.EntryList, 207 | subtrees: NestedTreeList, 208 | path: []const u8, 209 | 210 | pub fn init(allocator: mem.Allocator, path: []const u8) !NestedTree { 211 | return .{ 212 | .allocator = allocator, 213 | .entries = Tree.EntryList.init(allocator), 214 | .subtrees = NestedTreeList.init(allocator), 215 | .path = path, 216 | }; 217 | } 218 | 219 | pub fn toTree(self: *NestedTree, git_dir_path: []const u8) ![20]u8 { 220 | 221 | if (self.subtrees.items.len == 0) { 222 | const tree = Tree{ 223 | .allocator = self.allocator, 224 | .entries = self.entries.items, 225 | }; 226 | return writeTree(self.allocator, git_dir_path, tree); 227 | } 228 | 229 | for (self.subtrees.items) |*subtree| { 230 | var child_object_name = try subtree.toTree(git_dir_path); 231 | 232 | var entry = Tree.Entry{ 233 | .mode = Index.Mode{ 234 | .unix_permissions = 0, 235 | .object_type = .tree, 236 | }, 237 | .path = subtree.path, 238 | .object_name = child_object_name, 239 | }; 240 | try self.entries.append(entry); 241 | } 242 | 243 | const tree = Tree{ 244 | .allocator = self.allocator, 245 | .entries = self.entries.items, 246 | }; 247 | 248 | return writeTree(self.allocator, git_dir_path, tree); 249 | } 250 | }; 251 | 252 | const NestedTreeList = std.ArrayList(NestedTree); 253 | 254 | /// Returns a TreeWalker 255 | pub fn walkTree(allocator: mem.Allocator, git_dir_path: []const u8, tree_object_name: [20]u8) !TreeWalker { 256 | return TreeWalker.init(allocator, git_dir_path, tree_object_name); 257 | } 258 | 259 | /// Iterates over the contents of a tree 260 | pub const TreeWalker = struct { 261 | allocator: mem.Allocator, 262 | tree_stack: TreeList, 263 | index_stack: IndexList, 264 | path_stack: StringList, 265 | name_buffer: [fs.MAX_PATH_BYTES]u8, 266 | name_index: usize, 267 | git_dir_path: []const u8, 268 | 269 | pub fn init(allocator: mem.Allocator, git_dir_path: []const u8, tree_object_name: [20]u8) !TreeWalker { 270 | var tree_walker = TreeWalker{ 271 | .allocator = allocator, 272 | .tree_stack = TreeList.init(allocator), 273 | .index_stack = IndexList.init(allocator), 274 | .path_stack = StringList.init(allocator), 275 | .name_buffer = undefined, 276 | .name_index = 0, 277 | .git_dir_path = git_dir_path, 278 | }; 279 | const tree = try readTree(allocator, git_dir_path, tree_object_name); 280 | try tree_walker.tree_stack.append(tree); 281 | try tree_walker.index_stack.append(0); 282 | 283 | return tree_walker; 284 | } 285 | 286 | pub fn deinit(self: *TreeWalker) void { 287 | for (self.tree_stack.items) |tree| { 288 | tree.deinit(); 289 | } 290 | self.tree_stack.deinit(); 291 | self.index_stack.deinit(); 292 | for (self.path_stack.items) |path_item| { 293 | self.allocator.free(path_item); 294 | } 295 | self.path_stack.deinit(); 296 | } 297 | 298 | pub fn next(self: *TreeWalker) !?Tree.Entry { 299 | if (self.tree_stack.items.len == 0) { 300 | return null; 301 | } 302 | 303 | const tree_ptr: *Tree = &self.tree_stack.items[self.tree_stack.items.len - 1]; 304 | const index_ptr: *usize = &self.index_stack.items[self.index_stack.items.len - 1]; 305 | 306 | const orig_entry = tree_ptr.entries[index_ptr.*]; 307 | 308 | var buffer_alloc = std.heap.FixedBufferAllocator.init(&self.name_buffer); 309 | 310 | try self.path_stack.append(orig_entry.path); 311 | 312 | const entry_path = try fs.path.join(buffer_alloc.allocator(), self.path_stack.items); 313 | const entry = Tree.Entry{ 314 | .mode = orig_entry.mode, 315 | .object_name = orig_entry.object_name, 316 | .path = entry_path, 317 | }; 318 | 319 | _ = self.path_stack.pop(); 320 | 321 | index_ptr.* += 1; 322 | 323 | if (entry.mode.object_type == .tree) { 324 | var new_tree = try readTree(self.allocator, self.git_dir_path, entry.object_name); 325 | try self.tree_stack.append(new_tree); 326 | try self.path_stack.append(try self.allocator.dupe(u8, orig_entry.path)); 327 | try self.index_stack.append(0); 328 | } 329 | while (self.tree_stack.items.len > 0 and self.endOfTree()) { 330 | const tree = self.tree_stack.pop(); 331 | tree.deinit(); 332 | _ = self.index_stack.pop(); 333 | if (self.path_stack.items.len != 0) { 334 | const path = self.path_stack.pop(); 335 | self.allocator.free(path); 336 | } 337 | } 338 | 339 | return entry; 340 | } 341 | 342 | fn endOfTree(self: TreeWalker) bool { 343 | const tree_ptr: *Tree = &self.tree_stack.items[self.tree_stack.items.len - 1]; 344 | const index_ptr: *usize = &self.index_stack.items[self.index_stack.items.len - 1]; 345 | return index_ptr.* == tree_ptr.entries.len; 346 | } 347 | }; 348 | 349 | /// Returns the object that contains the file at a path in a tree 350 | pub fn entryFromTree(allocator: mem.Allocator, git_dir_path: []const u8, tree_object_name: [20]u8, path: []const u8) ![20]u8 { 351 | var path_iter = mem.split(u8, path, fs.path.sep_str); 352 | var tree_stack = TreeList.init(allocator); 353 | defer { 354 | for (tree_stack.items) |stack_tree| { 355 | stack_tree.deinit(); 356 | } 357 | tree_stack.deinit(); 358 | } 359 | 360 | const tree = try readTree(allocator, git_dir_path, tree_object_name); 361 | try tree_stack.append(tree); 362 | 363 | while (path_iter.next()) |path_segment| { 364 | const current_tree = tree_stack.items[tree_stack.items.len - 1]; 365 | const final_segment = path_iter.index == null; 366 | 367 | const found_entry = blk: { 368 | for (current_tree.entries) |entry| { 369 | if (mem.eql(u8, entry.path, path_segment)) { 370 | break :blk entry; 371 | } 372 | } 373 | break :blk null; 374 | }; 375 | 376 | if (found_entry == null) { 377 | return error.NoFileInTree; 378 | } 379 | 380 | if (final_segment and found_entry.?.mode.object_type == .tree) { 381 | return error.EntryIsTree; 382 | } 383 | 384 | if (final_segment and found_entry.?.mode.object_type != .tree) { 385 | return found_entry.?.object_name; 386 | } 387 | 388 | if (found_entry.?.mode.object_type == .tree) { 389 | const new_tree = try readTree(allocator, git_dir_path, found_entry.?.object_name); 390 | try tree_stack.append(new_tree); 391 | } 392 | } 393 | 394 | return error.NoFileInTree; 395 | } 396 | --------------------------------------------------------------------------------