╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
12 }; 13 14 fn commandHelp(command_kind: CommandKind) Help { 15 return switch (command_kind) { 16 .serve => .{ 17 .name = "serve", 18 .descrip = 19 \\a long-running server forwarding receive-pack and upload-pack. 20 , 21 .example = 22 \\haxy serve --web-listen 127.0.0.1:8080 --ssh-listen 127.0.0.1:8022 --data-dir /srv/git 23 \\use "none" for --ssh-listen to disable ssh. git clones over HTTP share the web UI's listener. 24 \\--web-url and --ssh-url set the public urls shown to users, like https://example.com and ssh://example.com. 25 \\--max-repo-size caps each repo's on-disk size, like 1G, rejecting pushes past it. no limit by default. 26 , 27 }, 28 }; 29 } 30 31 pub fn printHelp(cmd_kind_maybe: ?CommandKind, writer: *std.Io.Writer) !void { 32 const print_indent = comptime blk: { 33 var indent = 0; 34 for (0..@typeInfo(CommandKind).@"enum".field_names.len) |i| { 35 indent = @max(commandHelp(@fromBackingInt(@intCast(i))).name.len, indent); 36 } 37 indent += 2; 38 break :blk indent; 39 }; 40 41 if (cmd_kind_maybe) |cmd_kind| { 42 const help = commandHelp(cmd_kind); 43 // name and description 44 try writer.print("{s}", .{help.name}); 45 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 46 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 47 try writer.print("{s}\n", .{split_iter.first()}); 48 while (split_iter.next()) |line| { 49 for (0..print_indent) |_| try writer.print(" ", .{}); 50 try writer.print("{s}\n", .{line}); 51 } 52 try writer.print("\n", .{}); 53 // example 54 split_iter = std.mem.splitScalar(u8, help.example, '\n'); 55 while (split_iter.next()) |line| { 56 for (0..print_indent) |_| try writer.print(" ", .{}); 57 try writer.print("{s}\n", .{line}); 58 } 59 } else { 60 try writer.print("help: haxy <command> [<args>]\n\n", .{}); 61 inline for (@typeInfo(CommandKind).@"enum".field_values) |value| { 62 const help = commandHelp(@fromBackingInt(@intCast(value))); 63 // name and description 64 try writer.print("{s}", .{help.name}); 65 for (0..print_indent - help.name.len) |_| try writer.print(" ", .{}); 66 var split_iter = std.mem.splitScalar(u8, help.descrip, '\n'); 67 try writer.print("{s}\n", .{split_iter.first()}); 68 while (split_iter.next()) |line| { 69 for (0..print_indent) |_| try writer.print(" ", .{}); 70 try writer.print("{s}\n", .{line}); 71 } 72 } 73 } 74 } 75 76 pub const CommandArgs = struct { 77 allocator: std.mem.Allocator, 78 arena: *std.heap.ArenaAllocator, 79 command_kind: ?CommandKind, 80 command_name: ?[]const u8, 81 positional_args: []const []const u8, 82 map_args: std.StringArrayHashMapUnmanaged(?[]const u8), 83 unused_args: std.StringArrayHashMapUnmanaged(void), 84 85 // flags that can have a value associated with them 86 // must be included here 87 const value_flags = std.StaticStringMap(void).initComptime(.{ 88 .{"--web-listen"}, 89 .{"--ssh-listen"}, 90 .{"--web-url"}, 91 .{"--ssh-url"}, 92 .{"--max-repo-size"}, 93 .{"--data-dir"}, 94 }); 95 96 pub fn init(allocator: std.mem.Allocator, args: []const []const u8) !CommandArgs { 97 const arena = try allocator.create(std.heap.ArenaAllocator); 98 arena.* = std.heap.ArenaAllocator.init(allocator); 99 errdefer { 100 arena.deinit(); 101 allocator.destroy(arena); 102 } 103 104 var positional_args: std.ArrayList([]const u8) = .empty; 105 var map_args: std.StringArrayHashMapUnmanaged(?[]const u8) = .empty; 106 var unused_args: std.StringArrayHashMapUnmanaged(void) = .empty; 107 108 for (args) |arg| { 109 if (arg.len > 1 and arg[0] == '-') { 110 try map_args.put(arena.allocator(), arg, null); 111 try unused_args.put(arena.allocator(), arg, {}); 112 } else { 113 // if the last key is a value flag and doesn't have a value yet, 114 // set this arg as its value 115 const keys = map_args.keys(); 116 if (keys.len > 0) { 117 const last_key = keys[keys.len - 1]; 118 if (map_args.get(last_key)) |last_val| { 119 if (value_flags.has(last_key) and last_val == null) { 120 try map_args.put(arena.allocator(), last_key, arg); 121 continue; 122 } 123 } 124 } 125 126 // in any other case, just consider it a positional arg 127 try positional_args.append(arena.allocator(), arg); 128 } 129 } 130 131 const args_slice = try positional_args.toOwnedSlice(arena.allocator()); 132 if (args_slice.len == 0) { 133 return .{ 134 .allocator = allocator, 135 .arena = arena, 136 .command_kind = null, 137 .command_name = null, 138 .positional_args = args_slice, 139 .map_args = map_args, 140 .unused_args = unused_args, 141 }; 142 } else { 143 const command_name = args_slice[0]; 144 const extra_args = args_slice[1..]; 145 146 const command_kind: ?CommandKind = inline for (0..@typeInfo(CommandKind).@"enum".field_names.len) |i| { 147 if (std.mem.eql(u8, command_name, commandHelp(@fromBackingInt(@intCast(i))).name)) { 148 break @fromBackingInt(@intCast(i)); 149 } 150 } else null; 151 152 return .{ 153 .allocator = allocator, 154 .arena = arena, 155 .command_kind = command_kind, 156 .command_name = command_name, 157 .positional_args = extra_args, 158 .map_args = map_args, 159 .unused_args = unused_args, 160 }; 161 } 162 } 163 164 pub fn deinit(self: *CommandArgs) void { 165 self.arena.deinit(); 166 self.allocator.destroy(self.arena); 167 } 168 169 pub fn contains(self: *CommandArgs, arg: []const u8) bool { 170 _ = self.unused_args.orderedRemove(arg); 171 return self.map_args.contains(arg); 172 } 173 174 pub fn get(self: *CommandArgs, comptime arg: []const u8) ??[]const u8 { 175 comptime std.debug.assert(value_flags.has(arg)); // can only call `get` with flags included in `value_flags` 176 _ = self.unused_args.orderedRemove(arg); 177 return self.map_args.get(arg); 178 } 179 }; 180 181 /// parses the args into a format that can be directly used by a repo. 182 /// if any additional allocation needs to be done, the arena inside the cmd args will be used. 183 pub const Command = union(CommandKind) { 184 serve: srv.Options, 185 186 pub fn initMaybe(cmd_args: *CommandArgs) !?Command { 187 const command_kind = cmd_args.command_kind orelse return null; 188 switch (command_kind) { 189 .serve => { 190 return .{ .serve = try serveOptions(cmd_args) orelse return null }; 191 }, 192 } 193 } 194 }; 195 196 pub fn parseServeOptions(allocator: std.mem.Allocator, args: []const []const u8) !srv.Options { 197 var cmd_args = try CommandArgs.init(allocator, args); 198 defer cmd_args.deinit(); 199 const options = try serveOptions(&cmd_args) orelse return error.InvalidServeArgument; 200 if (cmd_args.unused_args.count() != 0) return error.InvalidServeArgument; 201 return options; 202 } 203 204 fn serveOptions(cmd_args: *CommandArgs) !?srv.Options { 205 if (cmd_args.positional_args.len != 0) return null; 206 207 var options: srv.Options = .{}; 208 if (cmd_args.get("--web-listen")) |value_maybe| options.web_listen = value_maybe orelse return error.WebListenNeedsValue; 209 if (cmd_args.get("--ssh-listen")) |value_maybe| { 210 const value = value_maybe orelse return error.SshListenNeedsValue; 211 options.ssh_listen = if (std.mem.eql(u8, value, "none")) null else value; 212 } 213 // urls are joined with "/" + a path 214 if (cmd_args.get("--web-url")) |value_maybe| options.web_url = std.mem.trimEnd(u8, value_maybe orelse return error.WebUrlNeedsValue, "/"); 215 if (cmd_args.get("--ssh-url")) |value_maybe| options.ssh_url = std.mem.trimEnd(u8, value_maybe orelse return error.SshUrlNeedsValue, "/"); 216 if (cmd_args.get("--max-repo-size")) |value_maybe| options.max_repo_size = try parseSize(value_maybe orelse return error.MaxRepoSizeNeedsValue); 217 if (cmd_args.get("--data-dir")) |value_maybe| options.data_dir = value_maybe orelse return error.DataDirNeedsValue; 218 return options; 219 } 220 221 // a byte count with an optional K, M or G suffix 222 fn parseSize(value: []const u8) !u64 { 223 if (value.len == 0) return error.InvalidSize; 224 const shift: u6 = switch (value[value.len - 1]) { 225 'K', 'k' => 10, 226 'M', 'm' => 20, 227 'G', 'g' => 30, 228 else => 0, 229 }; 230 const digits = if (shift == 0) value else value[0 .. value.len - 1]; 231 const count = std.fmt.parseInt(u64, digits, 10) catch return error.InvalidSize; 232 return std.math.shlExact(u64, count, shift) catch error.InvalidSize; 233 } 234 235 /// parses the given args into a command if valid, and determines how it should be run 236 /// (via the TUI or CLI). 237 pub const CommandDispatch = union(enum) { 238 invalid: union(enum) { 239 command: []const u8, 240 argument: struct { 241 command: ?CommandKind, 242 value: []const u8, 243 }, 244 }, 245 help: ?CommandKind, 246 local, 247 cli: Command, 248 249 pub fn init(cmd_args: *CommandArgs) !CommandDispatch { 250 const dispatch = try initIgnoreUnused(cmd_args); 251 if (cmd_args.unused_args.count() > 0) { 252 return .{ 253 .invalid = .{ 254 .argument = .{ 255 .command = switch (dispatch) { 256 .invalid => return dispatch, // if there was already an error, return it instead 257 .help => |cmd_kind_maybe| cmd_kind_maybe, 258 .local => null, 259 .cli => |command| command, 260 }, 261 .value = cmd_args.unused_args.keys()[0], 262 }, 263 }, 264 }; 265 } 266 return dispatch; 267 } 268 269 pub fn initIgnoreUnused(cmd_args: *CommandArgs) !CommandDispatch { 270 const show_help = cmd_args.contains("--help"); 271 272 if (cmd_args.command_kind) |command_kind| { 273 if (show_help) { 274 return .{ .help = command_kind }; 275 } else if (try Command.initMaybe(cmd_args)) |cmd| { 276 return .{ .cli = cmd }; 277 } else { 278 return .{ .help = command_kind }; 279 } 280 } else if (cmd_args.command_name) |command_name| { 281 return .{ .invalid = .{ .command = command_name } }; 282 } else if (show_help) { 283 return .{ .help = null }; 284 } else { 285 return .local; 286 } 287 } 288 }; 289