╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ diff3.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
4 const std = @import("std"); 5 const evt = @import("./event.zig"); 6 const xit = @import("xit"); 7 const rp = xit.repo; 8 const df = xit.diff; 9 10 // any kind works for buffer-backed iterators; the line size cap must admit a 11 // description that is one long line 12 const repo_kind: rp.RepoKind = .git; 13 const repo_opts: rp.RepoOpts(repo_kind) = .{ .max_line_size = evt.max_event_size }; 14 15 const LineIterator = df.LineIterator(repo_kind, repo_opts); 16 17 pub const Chunk = union(enum) { 18 // both sides agree on this text 19 same: []const u8, 20 // only one side changed it, so its version stands; null means it removed 21 // the lines 22 auto: struct { text: ?[]const u8, theirs: bool }, 23 // both sides changed it differently; null means that side removed the lines 24 conflict: struct { ours: ?[]const u8, theirs: ?[]const u8 }, 25 }; 26 27 // split base/ours/theirs into a chunk sequence. chunk texts are allocated in 28 // `arena` and hold whole lines joined with newlines. 29 pub fn chunks( 30 io: std.Io, 31 gpa: std.mem.Allocator, 32 arena: *std.heap.ArenaAllocator, 33 base: []const u8, 34 ours: []const u8, 35 theirs: []const u8, 36 ) ![]Chunk { 37 const aa = arena.allocator(); 38 39 var base_iter = try LineIterator.initFromTestBuffer(io, gpa, base); 40 defer base_iter.deinit(); 41 var ours_iter = try LineIterator.initFromTestBuffer(io, gpa, ours); 42 defer ours_iter.deinit(); 43 var theirs_iter = try LineIterator.initFromTestBuffer(io, gpa, theirs); 44 defer theirs_iter.deinit(); 45 46 // invalid utf-8 degrades an iterator to .binary with no lines, which would 47 // read as an empty side; treat the whole text as one conflict instead 48 if (base_iter.source == .binary or ours_iter.source == .binary or theirs_iter.source == .binary) { 49 const list = try aa.alloc(Chunk, 1); 50 list[0] = .{ .conflict = .{ 51 .ours = if (ours.len > 0) try aa.dupe(u8, ours) else null, 52 .theirs = if (theirs.len > 0) try aa.dupe(u8, theirs) else null, 53 } }; 54 return list; 55 } 56 57 var diff3_iter = try df.Diff3Iterator(repo_kind, repo_opts).init(gpa, &base_iter, &ours_iter, &theirs_iter); 58 defer diff3_iter.deinit(); 59 60 var list: std.ArrayList(Chunk) = .empty; 61 while (try diff3_iter.next()) |chunk| { 62 switch (chunk) { 63 .clean => |range| try list.append(aa, .{ .same = (try joinRange(aa, &base_iter, range)) orelse unreachable }), 64 .conflict => |ranges| { 65 // the iterator marks every non-identical chunk a conflict, so 66 // one-sided changes are classified here, like xit's merge does 67 const base_text = try joinRange(aa, &base_iter, ranges.o_range); 68 const ours_text = try joinRange(aa, &ours_iter, ranges.a_range); 69 const theirs_text = try joinRange(aa, &theirs_iter, ranges.b_range); 70 if (textEqual(ours_text, theirs_text)) { 71 // the same change on both sides; nothing left = no chunk 72 if (ours_text) |text| try list.append(aa, .{ .same = text }); 73 } else if (textEqual(base_text, ours_text)) { 74 try list.append(aa, .{ .auto = .{ .text = theirs_text, .theirs = true } }); 75 } else if (textEqual(base_text, theirs_text)) { 76 try list.append(aa, .{ .auto = .{ .text = ours_text, .theirs = false } }); 77 } else { 78 try list.append(aa, .{ .conflict = .{ .ours = ours_text, .theirs = theirs_text } }); 79 } 80 }, 81 } 82 } 83 return try list.toOwnedSlice(aa); 84 } 85 86 // join the chunks back into one document. `resolutions` holds the final text 87 // of each conflict chunk in order; an empty resolution drops the hunk's lines. 88 pub fn assemble( 89 allocator: std.mem.Allocator, 90 chunk_list: []const Chunk, 91 resolutions: []const []const u8, 92 ) ![]u8 { 93 var out: std.ArrayList(u8) = .empty; 94 errdefer out.deinit(allocator); 95 var resolution_index: usize = 0; 96 for (chunk_list) |chunk| { 97 const piece: ?[]const u8 = switch (chunk) { 98 .same => |text| text, 99 .auto => |auto| auto.text, 100 .conflict => blk: { 101 const resolution = resolutions[resolution_index]; 102 resolution_index += 1; 103 break :blk if (resolution.len > 0) resolution else null; 104 }, 105 }; 106 if (piece) |text| { 107 if (out.items.len > 0) try out.append(allocator, '\n'); 108 try out.appendSlice(allocator, text); 109 } 110 } 111 return try out.toOwnedSlice(allocator); 112 } 113 114 // a range's lines joined with newlines, or null when it spans no lines 115 fn joinRange( 116 aa: std.mem.Allocator, 117 iter: *LineIterator, 118 range_maybe: ?df.Diff3Iterator(repo_kind, repo_opts).Range, 119 ) !?[]const u8 { 120 const range = range_maybe orelse return null; 121 if (range.end <= range.begin) return null; 122 var out: std.ArrayList(u8) = .empty; 123 for (range.begin..range.end) |line_num| { 124 // buffer-backed lines are borrowed, so no free is needed 125 const line = try iter.get(line_num); 126 if (line_num > range.begin) try out.append(aa, '\n'); 127 try out.appendSlice(aa, line); 128 } 129 return try out.toOwnedSlice(aa); 130 } 131 132 fn textEqual(a: ?[]const u8, b: ?[]const u8) bool { 133 const a_text = a orelse return b == null; 134 const b_text = b orelse return false; 135 return std.mem.eql(u8, a_text, b_text); 136 } 137