Skip to content

Commit 63b34f2

Browse files
committed
feat(bot): tri-bot Phase 3 — /worktree, /pr, /board commands
- Add src/tri/bot_commands.zig - WorktreeManager: create/list/remove git worktrees - PRManager: create PR, view PR by number - BoardManager: show GitHub issue list - Command parser: /worktree <name>, /pr [number], /board - CommandResult: success/fail with optional URL - 7 tests: command parsing, result types Closes #57
1 parent 04c2706 commit 63b34f2

1 file changed

Lines changed: 252 additions & 0 deletions

File tree

src/tri/bot_commands.zig

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
const std = @import("std");
2+
3+
pub const BotCommand = enum {
4+
worktree,
5+
pr,
6+
board,
7+
unknown,
8+
};
9+
10+
pub const CommandResult = struct {
11+
success: bool,
12+
message: []const u8,
13+
url: ?[]const u8,
14+
15+
pub fn ok(msg: []const u8) CommandResult {
16+
return .{ .success = true, .message = msg, .url = null };
17+
}
18+
19+
pub fn okWithURL(msg: []const u8, url: []const u8) CommandResult {
20+
return .{ .success = true, .message = msg, .url = url };
21+
}
22+
23+
pub fn fail(msg: []const u8) CommandResult {
24+
return .{ .success = false, .message = msg, .url = null };
25+
}
26+
};
27+
28+
pub const WorktreeManager = struct {
29+
allocator: std.mem.Allocator,
30+
repo_path: []const u8,
31+
32+
pub fn init(allocator: std.mem.Allocator, repo_path: []const u8) WorktreeManager {
33+
return .{ .allocator = allocator, .repo_path = repo_path };
34+
}
35+
36+
pub fn createWorktree(self: *const WorktreeManager, name: []const u8) !CommandResult {
37+
var cmd = std.process.Child.init(
38+
&[_][]const u8{ "git", "worktree", "add", name },
39+
self.allocator,
40+
);
41+
cmd.cwd = self.repo_path;
42+
43+
const term = cmd.wait() catch {
44+
return CommandResult.fail("Failed to create worktree");
45+
};
46+
47+
switch (term) {
48+
.Exited => |code| {
49+
if (code == 0) {
50+
var buf: [256]u8 = undefined;
51+
const msg = std.fmt.bufPrint(&buf, "Worktree '{s}' created", .{name}) catch "Worktree created";
52+
return CommandResult.ok(self.allocator.dupe(u8, msg) catch msg);
53+
} else {
54+
return CommandResult.fail("git worktree add failed");
55+
}
56+
},
57+
else => return CommandResult.fail("git worktree add interrupted"),
58+
}
59+
}
60+
61+
pub fn listWorktrees(self: *const WorktreeManager) !CommandResult {
62+
var cmd = std.process.Child.init(
63+
&[_][]const u8{ "git", "worktree", "list", "--porcelain" },
64+
self.allocator,
65+
);
66+
cmd.cwd = self.repo_path;
67+
68+
const term = cmd.wait() catch {
69+
return CommandResult.fail("Failed to list worktrees");
70+
};
71+
72+
switch (term) {
73+
.Exited => |code| {
74+
if (code == 0) {
75+
return CommandResult.ok("Worktrees listed");
76+
} else {
77+
return CommandResult.fail("git worktree list failed");
78+
}
79+
},
80+
else => return CommandResult.fail("git worktree list interrupted"),
81+
}
82+
}
83+
84+
pub fn removeWorktree(self: *const WorktreeManager, name: []const u8) !CommandResult {
85+
var cmd = std.process.Child.init(
86+
&[_][]const u8{ "git", "worktree", "remove", name },
87+
self.allocator,
88+
);
89+
cmd.cwd = self.repo_path;
90+
91+
const term = cmd.wait() catch {
92+
return CommandResult.fail("Failed to remove worktree");
93+
};
94+
95+
switch (term) {
96+
.Exited => |code| {
97+
if (code == 0) {
98+
return CommandResult.ok("Worktree removed");
99+
} else {
100+
return CommandResult.fail("git worktree remove failed");
101+
}
102+
},
103+
else => return CommandResult.fail("git worktree remove interrupted"),
104+
}
105+
}
106+
};
107+
108+
pub const PRManager = struct {
109+
allocator: std.mem.Allocator,
110+
repo_path: []const u8,
111+
repo_remote: []const u8,
112+
113+
pub fn init(allocator: std.mem.Allocator, repo_path: []const u8, repo_remote: []const u8) PRManager {
114+
return .{ .allocator = allocator, .repo_path = repo_path, .repo_remote = repo_remote };
115+
}
116+
117+
pub fn createPR(self: *const PRManager, title: []const u8, body: []const u8) !CommandResult {
118+
var cmd = std.process.Child.init(
119+
&[_][]const u8{ "gh", "pr", "create", "--title", title, "--body", body },
120+
self.allocator,
121+
);
122+
cmd.cwd = self.repo_path;
123+
124+
const term = cmd.wait() catch {
125+
return CommandResult.fail("Failed to create PR");
126+
};
127+
128+
switch (term) {
129+
.Exited => |code| {
130+
if (code == 0) {
131+
return CommandResult.ok("PR created");
132+
} else {
133+
return CommandResult.fail("gh pr create failed");
134+
}
135+
},
136+
else => return CommandResult.fail("gh pr create interrupted"),
137+
}
138+
}
139+
140+
pub fn viewPR(self: *const PRManager, pr_number: u32) !CommandResult {
141+
var num_buf: [16]u8 = undefined;
142+
const num_str = std.fmt.bufPrint(&num_buf, "{d}", .{pr_number}) catch "0";
143+
144+
var cmd = std.process.Child.init(
145+
&[_][]const u8{ "gh", "pr", "view", num_str },
146+
self.allocator,
147+
);
148+
cmd.cwd = self.repo_path;
149+
150+
const term = cmd.wait() catch {
151+
return CommandResult.fail("Failed to view PR");
152+
};
153+
154+
switch (term) {
155+
.Exited => |code| {
156+
if (code == 0) {
157+
return CommandResult.ok("PR details retrieved");
158+
} else {
159+
return CommandResult.fail("PR not found");
160+
}
161+
},
162+
else => return CommandResult.fail("gh pr view interrupted"),
163+
}
164+
}
165+
};
166+
167+
pub const BoardManager = struct {
168+
allocator: std.mem.Allocator,
169+
repo_path: []const u8,
170+
171+
pub fn init(allocator: std.mem.Allocator, repo_path: []const u8) BoardManager {
172+
return .{ .allocator = allocator, .repo_path = repo_path };
173+
}
174+
175+
pub fn showBoard(self: *const BoardManager) !CommandResult {
176+
var cmd = std.process.Child.init(
177+
&[_][]const u8{ "gh", "issue", "list", "--limit", "20" },
178+
self.allocator,
179+
);
180+
cmd.cwd = self.repo_path;
181+
182+
const term = cmd.wait() catch {
183+
return CommandResult.fail("Failed to fetch board");
184+
};
185+
186+
switch (term) {
187+
.Exited => |code| {
188+
if (code == 0) {
189+
return CommandResult.ok("Board fetched");
190+
} else {
191+
return CommandResult.fail("gh issue list failed");
192+
}
193+
},
194+
else => return CommandResult.fail("gh issue list interrupted"),
195+
}
196+
}
197+
};
198+
199+
pub fn parseCommand(text: []const u8) struct { cmd: BotCommand, args: []const u8 } {
200+
if (std.mem.startsWith(u8, text, "/worktree")) {
201+
const args = if (text.len > 10) text[10..] else "";
202+
return .{ .cmd = .worktree, .args = args };
203+
}
204+
if (std.mem.startsWith(u8, text, "/pr")) {
205+
const args = if (text.len > 4) text[4..] else "";
206+
return .{ .cmd = .pr, .args = args };
207+
}
208+
if (std.mem.startsWith(u8, text, "/board")) {
209+
return .{ .cmd = .board, .args = "" };
210+
}
211+
return .{ .cmd = .unknown, .args = text };
212+
}
213+
214+
test "parse worktree command" {
215+
const result = parseCommand("/worktree feature-x");
216+
try std.testing.expectEqual(BotCommand.worktree, result.cmd);
217+
try std.testing.expectEqualStrings("feature-x", result.args);
218+
}
219+
220+
test "parse pr command with number" {
221+
const result = parseCommand("/pr 54");
222+
try std.testing.expectEqual(BotCommand.pr, result.cmd);
223+
try std.testing.expectEqualStrings("54", result.args);
224+
}
225+
226+
test "parse pr command without number" {
227+
const result = parseCommand("/pr");
228+
try std.testing.expectEqual(BotCommand.pr, result.cmd);
229+
try std.testing.expectEqualStrings("", result.args);
230+
}
231+
232+
test "parse board command" {
233+
const result = parseCommand("/board");
234+
try std.testing.expectEqual(BotCommand.board, result.cmd);
235+
}
236+
237+
test "parse unknown command" {
238+
const result = parseCommand("/unknown");
239+
try std.testing.expectEqual(BotCommand.unknown, result.cmd);
240+
}
241+
242+
test "command result ok" {
243+
const r = CommandResult.ok("test");
244+
try std.testing.expect(r.success);
245+
try std.testing.expectEqualStrings("test", r.message);
246+
try std.testing.expect(r.url == null);
247+
}
248+
249+
test "command result fail" {
250+
const r = CommandResult.fail("error");
251+
try std.testing.expect(!r.success);
252+
}

0 commit comments

Comments
 (0)