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