╭ command.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const hash = xit.hash; 5 6 pub const CommandKind = enum { 7 upload_pack, 8 receive_pack, 9 http_backend, 10 serve, 11 }; 12 13 const Help = struct { 14 name: []const u8, 15 descrip: []const u8, 16 example: []const u8, 17 }; 18 19 fn commandHelp(command_kind: CommandKind) Help { 20 return switch (command_kind) { 21 .upload_pack => .{ 22 .name = "upload-pack", 23 .descrip = 24 \\send what is fetched from the repository. 25 , 26 .example = 27 \\haxy upload-pack <directory> 28 , 29 }, 30 .receive_pack => .{ 31 .name = "receive-pack", 32 .descrip = 33 \\receive what is pushed into the repository. 34 , 35 .example = 36 \\haxy receive-pack <directory> 37 , 38 }, 39 .http_backend => .{ 40 .name = "http-backend", 41 .descrip = 42 \\a CGI program forwarding receive-pack and upload-pack over HTTP. 43 , 44 .example = 45 \\haxy http-backend 46 , 47 }, 48 .serve => .{ 49 .name = "serve", 50 .descrip = 51 \\a long-running HTTP server forwarding receive-pack and upload-pack. 52 , 53 .example = 54 \\haxy serve --http-listen 127.0.0.1:8080 --project-root /srv/git 55 , 56 }, 57 }; 58 } 59 60 pub fn printHelp(cmd_kind_maybe: ?CommandKind, writer: *std.Io.Writer) !void { 61 const print_indent = comptime blk: { 62 var indent = 0; 63 for (0..@typeInfo(CommandKind).@"enum".fields.len) |i| { 64 indent = @max(commandHelp(@enumFromInt(i)).name.len, indent); 65 } 66 indent += 2; 67 break :blk indent; 68 }; 69 70 if (cmd_kind_maybe) |cmd_kind| { 71 const help = commandHelp(cmd_kind); 72 // name and description 73 try writer.print("{s}", .{help.name}); 74 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 75 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 76 try writer.print("{s}\n", .{split_iter.first()}); 77 while (split_iter.next()) |line| { 78 for (0..print_indent) |_| try writer.print(" ", .{}); 79 try writer.print("{s}\n", .{line}); 80 } 81 try writer.print("\n", .{}); 82 // example 83 split_iter = std.mem.splitScalar(u8, help.example, '\n'); 84 while (split_iter.next()) |line| { 85 for (0..print_indent) |_| try writer.print(" ", .{}); 86 try writer.print("{s}\n", .{line}); 87 } 88 } else { 89 try writer.print("help: xit <command> [<args>]\n\n", .{}); 90 inline for (@typeInfo(CommandKind).@"enum".fields) |field| { 91 const help = commandHelp(@enumFromInt(field.value)); 92 // name and description 93 try writer.print("{s}", .{help.name}); 94 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 95 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 96 try writer.print("{s}\n", .{split_iter.first()}); 97 while (split_iter.next()) |line| { 98 for (0..print_indent) |_| try writer.print(" ", .{}); 99 try writer.print("{s}\n", .{line}); 100 } 101 } 102 } 103 } 104 105 pub const CommandArgs = struct { 106 allocator: std.mem.Allocator, 107 arena: *std.heap.ArenaAllocator, 108 command_kind: ?CommandKind, 109 command_name: ?[]const u8, 110 positional_args: []const []const u8, 111 map_args: std.StringArrayHashMapUnmanaged(?[]const u8), 112 unused_args: std.StringArrayHashMapUnmanaged(void), 113 114 // flags that can have a value associated with them 115 // must be included here 116 const value_flags = std.StaticStringMap(void).initComptime(.{ 117 .{"--http-listen"}, 118 .{"--project-root"}, 119 }); 120 121 pub fn init(allocator: std.mem.Allocator, args: []const []const u8) !CommandArgs { 122 const arena = try allocator.create(std.heap.ArenaAllocator); 123 arena.* = std.heap.ArenaAllocator.init(allocator); 124 errdefer { 125 arena.deinit(); 126 allocator.destroy(arena); 127 } 128 129 var positional_args: std.ArrayList([]const u8) = .empty; 130 var map_args: std.StringArrayHashMapUnmanaged(?[]const u8) = .empty; 131 var unused_args: std.StringArrayHashMapUnmanaged(void) = .empty; 132 133 for (args) |arg| { 134 if (arg.len > 1 and arg[0] == '-') { 135 try map_args.put(arena.allocator(), arg, null); 136 try unused_args.put(arena.allocator(), arg, {}); 137 } else { 138 // if the last key is a value flag and doesn't have a value yet, 139 // set this arg as its value 140 const keys = map_args.keys(); 141 if (keys.len > 0) { 142 const last_key = keys[keys.len - 1]; 143 if (map_args.get(last_key)) |last_val| { 144 if (value_flags.has(last_key) and last_val == null) { 145 try map_args.put(arena.allocator(), last_key, arg); 146 continue; 147 } 148 } 149 } 150 151 // in any other case, just consider it a positional arg 152 try positional_args.append(arena.allocator(), arg); 153 } 154 } 155 156 const args_slice = try positional_args.toOwnedSlice(arena.allocator()); 157 if (args_slice.len == 0) { 158 return .{ 159 .allocator = allocator, 160 .arena = arena, 161 .command_kind = null, 162 .command_name = null, 163 .positional_args = args_slice, 164 .map_args = map_args, 165 .unused_args = unused_args, 166 }; 167 } else { 168 const command_name = args_slice[0]; 169 const extra_args = args_slice[1..]; 170 171 const command_kind: ?CommandKind = inline for (0..@typeInfo(CommandKind).@"enum".fields.len) |i| { 172 if (std.mem.eql(u8, command_name, commandHelp(@enumFromInt(i)).name)) { 173 break @enumFromInt(i); 174 } 175 } else null; 176 177 return .{ 178 .allocator = allocator, 179 .arena = arena, 180 .command_kind = command_kind, 181 .command_name = command_name, 182 .positional_args = extra_args, 183 .map_args = map_args, 184 .unused_args = unused_args, 185 }; 186 } 187 } 188 189 pub fn deinit(self: *CommandArgs) void { 190 self.arena.deinit(); 191 self.allocator.destroy(self.arena); 192 } 193 194 pub fn contains(self: *CommandArgs, arg: []const u8) bool { 195 _ = self.unused_args.orderedRemove(arg); 196 return self.map_args.contains(arg); 197 } 198 199 pub fn get(self: *CommandArgs, comptime arg: []const u8) ??[]const u8 { 200 comptime std.debug.assert(value_flags.has(arg)); // can only call `get` with flags included in `value_flags` 201 _ = self.unused_args.orderedRemove(arg); 202 return self.map_args.get(arg); 203 } 204 }; 205 206 /// parses the args into a format that can be directly used by a repo. 207 /// if any additional allocation needs to be done, the arena inside the cmd args will be used. 208 pub fn Command(comptime repo_kind: rp.RepoKind, comptime hash_kind: hash.HashKind) type { 209 return union(CommandKind) { 210 upload_pack: struct { 211 dir: []const u8, 212 options: xit.net_server_upload_pack.Options, 213 }, 214 receive_pack: struct { 215 dir: []const u8, 216 options: xit.net_server_receive_pack.Options, 217 }, 218 http_backend, 219 serve: struct { 220 http_listen: []const u8, 221 project_root: []const u8, 222 }, 223 224 pub fn initMaybe(cmd_args: *CommandArgs) !?Command(repo_kind, hash_kind) { 225 const command_kind = cmd_args.command_kind orelse return null; 226 switch (command_kind) { 227 .upload_pack => { 228 if (cmd_args.positional_args.len != 1) return null; 229 230 return .{ .upload_pack = .{ 231 .dir = cmd_args.positional_args[0], 232 .options = .{ 233 .advertise_refs = cmd_args.contains("--http-backend-info-refs"), 234 .is_stateless = cmd_args.contains("--stateless-rpc"), 235 }, 236 } }; 237 }, 238 .receive_pack => { 239 if (cmd_args.positional_args.len != 1) return null; 240 241 return .{ .receive_pack = .{ 242 .dir = cmd_args.positional_args[0], 243 .options = .{ 244 .skip_connectivity_check = cmd_args.contains("--skip-connectivity-check"), 245 .advertise_refs = cmd_args.contains("--http-backend-info-refs"), 246 .is_stateless = cmd_args.contains("--stateless-rpc"), 247 }, 248 } }; 249 }, 250 .http_backend => { 251 if (cmd_args.positional_args.len != 0) return null; 252 253 return .http_backend; 254 }, 255 .serve => { 256 if (cmd_args.positional_args.len != 0) return null; 257 258 return .{ .serve = .{ 259 .http_listen = (cmd_args.get("--http-listen") orelse null) orelse "127.0.0.1:8080", 260 .project_root = (cmd_args.get("--project-root") orelse null) orelse ".", 261 } }; 262 }, 263 } 264 } 265 }; 266 } 267 268 /// parses the given args into a command if valid, and determines how it should be run 269 /// (via the TUI or CLI). 270 pub fn CommandDispatch(comptime repo_kind: rp.RepoKind, comptime hash_kind: hash.HashKind) type { 271 return union(enum) { 272 invalid: union(enum) { 273 command: []const u8, 274 argument: struct { 275 command: ?CommandKind, 276 value: []const u8, 277 }, 278 }, 279 help: ?CommandKind, 280 cli: Command(repo_kind, hash_kind), 281 282 pub fn init(cmd_args: *CommandArgs) !CommandDispatch(repo_kind, hash_kind) { 283 const dispatch = try initIgnoreUnused(cmd_args); 284 if (cmd_args.unused_args.count() > 0) { 285 return .{ 286 .invalid = .{ 287 .argument = .{ 288 .command = switch (dispatch) { 289 .invalid => return dispatch, // if there was already an error, return it instead 290 .help => |cmd_kind_maybe| cmd_kind_maybe, 291 .cli => |command| command, 292 }, 293 .value = cmd_args.unused_args.keys()[0], 294 }, 295 }, 296 }; 297 } 298 return dispatch; 299 } 300 301 pub fn initIgnoreUnused(cmd_args: *CommandArgs) !CommandDispatch(repo_kind, hash_kind) { 302 const show_help = cmd_args.contains("--help"); 303 304 if (cmd_args.command_kind) |command_kind| { 305 if (show_help) { 306 return .{ .help = command_kind }; 307 } else if (try Command(repo_kind, hash_kind).initMaybe(cmd_args)) |cmd| { 308 return .{ .cli = cmd }; 309 } else { 310 return .{ .help = command_kind }; 311 } 312 } else if (cmd_args.command_name) |command_name| { 313 return .{ .invalid = .{ .command = command_name } }; 314 } else if (show_help) { 315 return .{ .help = null }; 316 } else { 317 return .{ .help = null }; 318 } 319 } 320 }; 321 } 322