╭ fork.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const hash = xit.hash; 6 const obj = xit.object; 7 const mrg = xit.merge; 8 9 pub const ref = xit.ref.Ref{ .kind = .head, .name = "patch" }; 10 11 const author_config = "haxy.author"; 12 const target_config = "haxy.target"; 13 14 pub const Route = struct { 15 identity: []const u8, 16 id: [evt.event_id_size * 2]u8, 17 target: []const u8, 18 }; 19 20 pub fn parseRoute(route_path: []const u8) ?Route { 21 const patch_segment = "/patch:"; 22 const branch_segment = "/branch:"; 23 const patch_start = std.mem.indexOf(u8, route_path, patch_segment) orelse return null; 24 const branch_start = std.mem.indexOfPos(u8, route_path, patch_start + patch_segment.len, branch_segment) orelse return null; 25 const identity = route_path[0..patch_start]; 26 const id_text = route_path[patch_start + patch_segment.len .. branch_start]; 27 const target = route_path[branch_start + branch_segment.len ..]; 28 if (identity.len == 0 or target.len == 0 or id_text.len != evt.event_id_size * 2) return null; 29 const id_bytes = evt.parseEventId(id_text) catch return null; 30 return .{ .identity = identity, .id = std.fmt.bytesToHex(id_bytes, .lower), .target = target }; 31 } 32 33 pub fn forkPath(allocator: std.mem.Allocator, repo_root_path: []const u8, id: []const u8) ![]u8 { 34 _ = try evt.parseEventId(id); 35 return try std.fs.path.join(allocator, &.{ std.fs.path.dirname(repo_root_path) orelse ".", "forks", id }); 36 } 37 38 pub fn create( 39 comptime repo_opts: rp.RepoOpts(.xit), 40 io: std.Io, 41 allocator: std.mem.Allocator, 42 repo_root_path: []const u8, 43 id: []const u8, 44 author_name: []const u8, 45 ) ![]u8 { 46 const draft_path = try forkPath(allocator, repo_root_path, id); 47 errdefer allocator.free(draft_path); 48 49 if (std.Io.Dir.accessAbsolute(io, draft_path, .{})) { 50 var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 51 defer repo.deinit(io, allocator); 52 const stored = try draftAuthor(repo_opts, &repo, io, allocator); 53 defer if (stored) |value| allocator.free(value); 54 const stored_author = stored orelse return error.InvalidPatchDraft; 55 if (!std.mem.eql(u8, stored_author, author_name)) return error.InvalidPatchDraft; 56 return draft_path; 57 } else |_| {} 58 59 try std.Io.Dir.cwd().createDirPath(io, std.fs.path.dirname(draft_path) orelse return error.InvalidPatchDraft); 60 var repo = try rp.Repo(.xit, repo_opts).init(io, allocator, .{ .path = draft_path, .create_default_branch = ref.name }); 61 defer repo.deinit(io, allocator); 62 try repo.addConfig(io, allocator, .{ .name = author_config, .value = author_name }); 63 try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 64 return draft_path; 65 } 66 67 pub fn draftAuthor( 68 comptime repo_opts: rp.RepoOpts(.xit), 69 repo: *rp.Repo(.xit, repo_opts), 70 io: std.Io, 71 allocator: std.mem.Allocator, 72 ) !?[]u8 { 73 return configValue(repo_opts, repo, io, allocator, author_config); 74 } 75 76 pub fn draftTarget( 77 comptime repo_opts: rp.RepoOpts(.xit), 78 repo: *rp.Repo(.xit, repo_opts), 79 io: std.Io, 80 allocator: std.mem.Allocator, 81 ) !?[]u8 { 82 return configValue(repo_opts, repo, io, allocator, target_config); 83 } 84 85 pub fn setTarget( 86 comptime repo_opts: rp.RepoOpts(.xit), 87 repo: *rp.Repo(.xit, repo_opts), 88 io: std.Io, 89 allocator: std.mem.Allocator, 90 value: []const u8, 91 ) !void { 92 try repo.addConfig(io, allocator, .{ .name = target_config, .value = value }); 93 } 94 95 fn configValue( 96 comptime repo_opts: rp.RepoOpts(.xit), 97 repo: *rp.Repo(.xit, repo_opts), 98 io: std.Io, 99 allocator: std.mem.Allocator, 100 name: []const u8, 101 ) !?[]u8 { 102 const dot = std.mem.indexOfScalar(u8, name, '.') orelse return error.InvalidConfigName; 103 var config = try repo.listConfig(io, allocator); 104 defer config.deinit(); 105 const variables = config.sections.get(name[0..dot]) orelse return null; 106 const value = variables.get(name[dot + 1 ..]) orelse return null; 107 return try allocator.dupe(u8, value); 108 } 109 110 pub const PublishInput = struct { 111 id: [evt.event_id_size * 2]u8, 112 title: []const u8, 113 tags: []const u8, 114 description: []const u8, 115 author: evt.CommitAuthor, 116 timestamp: u64, 117 }; 118 119 pub fn publish( 120 comptime repo_opts: rp.RepoOpts(.xit), 121 io: std.Io, 122 allocator: std.mem.Allocator, 123 target_repo: *rp.Repo(.xit, repo_opts), 124 draft_path: []const u8, 125 input: PublishInput, 126 ) !void { 127 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; 128 const patch_id = try evt.parseEventId(&input.id); 129 130 var arena = std.heap.ArenaAllocator.init(allocator); 131 defer arena.deinit(); 132 const moment_maybe = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 133 error.NotFound => null, 134 else => |other| return other, 135 }; 136 const existing = if (moment_maybe) |moment| try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) else null; 137 if (existing) |record| { 138 if (record.removed or !std.mem.eql(u8, record.author_email orelse "", input.author.email)) return error.InvalidPatch; 139 } 140 141 var stage = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = draft_path }); 142 defer stage.deinit(io, allocator); 143 const stored_author_maybe = try draftAuthor(repo_opts, &stage, io, allocator); 144 defer if (stored_author_maybe) |value| allocator.free(value); 145 const stored_author = stored_author_maybe orelse return error.InvalidPatchDraft; 146 if (!std.mem.eql(u8, stored_author, input.author.name)) return error.InvalidPatchDraft; 147 const target_name = (try draftTarget(repo_opts, &stage, io, allocator)) orelse return error.PatchNotPushed; 148 defer allocator.free(target_name); 149 if (!xit.ref.validateName(target_name)) return error.InvalidPatchDraft; 150 const source_oid = (try stage.readRef(io, ref)) orelse return error.PatchNotPushed; 151 152 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_name })) orelse return error.TargetNotFound; 153 var stage_moment = try stage.core.latestMoment(); 154 const stage_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &stage.core, .extra = .{ .moment = &stage_moment } }; 155 var objects = try obj.ObjectIterator(.xit, repo_opts).init(stage_state, io, allocator, .{ .kind = .all }); 156 defer objects.deinit(); 157 try objects.include(&source_oid); 158 try target_repo.copyObjects(.xit, repo_opts, &objects, io, null); 159 160 var target_moment = try target_repo.core.latestMoment(); 161 const target_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &target_moment } }; 162 const base_oid = try mrg.commonAncestor(.xit, repo_opts, target_state, io, allocator, &target_oid, &source_oid); 163 var base_object = try obj.Object(.xit, repo_opts).initCommit(target_state, io, allocator, &base_oid); 164 defer base_object.deinit(); 165 var source_object = try obj.Object(.xit, repo_opts).initCommit(target_state, io, allocator, &source_oid); 166 defer source_object.deinit(); 167 const base_tree_oid = base_object.content.commit.tree; 168 const head_tree_oid = source_object.content.commit.tree; 169 170 var message: std.ArrayList(u8) = .empty; 171 defer message.deinit(allocator); 172 try source_object.readMessage(allocator, &message, .limited(10_000)); 173 174 const target_ref = try std.fmt.allocPrint(arena.allocator(), "refs/heads/{s}", .{target_name}); 175 var revision_changed = true; 176 var patchrev_id: [evt.event_id_size]u8 = undefined; 177 if (existing) |record| { 178 patchrev_id = try evt.parseEventId(&record.event.patchrev_id); 179 if (std.mem.eql(u8, record.event.target_ref, target_ref)) { 180 const moment = moment_maybe orelse return error.PatchNotFound; 181 if (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patchrev_id)) |revision| { 182 revision_changed = !std.mem.eql(u8, revision.event.base_oid, &base_oid) or !std.mem.eql(u8, revision.event.source_oid, &source_oid); 183 } 184 } 185 } 186 if (revision_changed) io.random(&patchrev_id); 187 const patchrev_hex = std.fmt.bytesToHex(patchrev_id, .lower); 188 189 const tree_entries = [_]evt.EventTreeEntry{ 190 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 191 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 192 }; 193 const patch_event = evt.EventWithId{ 194 .id = input.id, 195 .timestamp = input.timestamp, 196 .author = input.author, 197 .event = .{ .patch = .{ 198 .title = input.title, 199 .description = input.description, 200 .tags = input.tags, 201 .target_ref = target_ref, 202 .target_patch_id = if (existing) |record| record.event.target_patch_id else null, 203 .patchrev_id = patchrev_hex, 204 } }, 205 }; 206 if (revision_changed) { 207 try evt.consume(.xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{ 208 .{ 209 .id = patchrev_hex, 210 .timestamp = input.timestamp, 211 .author = input.author, 212 .tree_entries = &tree_entries, 213 .event = .{ .patchrev = .{ .base_oid = &base_oid, .source_oid = &source_oid, .message = message.items } }, 214 }, 215 patch_event, 216 }); 217 } else { 218 try evt.consume(.xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{patch_event}); 219 } 220 } 221