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