| 24 | argsSchema = GitLogArgs; |
| 25 | |
| 26 | async execute(args: z.infer<typeof GitLogArgs>, ctx: ToolContext): Promise<ToolResult> { |
| 27 | if (!await isGitRepo(ctx.cwd, ctx.signal)) { |
| 28 | return { content: '[NOT_A_GIT_REPO] Current directory is not inside a git working tree.', isError: true }; |
| 29 | } |
| 30 | const limit = args.limit ?? 20; |
| 31 | // NUL-separated fields, newline-separated commits. Format codes: |
| 32 | // %h short sha, %ad author-date short, %an author name, %s subject |
| 33 | const gitArgs = ['log', `-${limit}`, '--date=short', `--pretty=format:%h%x00%ad%x00%an%x00%s`]; |
| 34 | if (args.author) gitArgs.push(`--author=${args.author}`); |
| 35 | if (args.since) gitArgs.push(`--since=${args.since}`); |
| 36 | if (args.branch) gitArgs.push(args.branch); |
| 37 | if (args.paths && args.paths.length > 0) { |
| 38 | gitArgs.push('--'); |
| 39 | gitArgs.push(...args.paths); |
| 40 | } |
| 41 | |
| 42 | const r = await git(gitArgs, { cwd: ctx.cwd, signal: ctx.signal }); |
| 43 | if (r.exitCode !== 0) { |
| 44 | // Empty repo, unknown ref, etc. |
| 45 | return { content: `[ERROR] ${r.stderr.trim() || r.stdout.trim()}`, isError: true }; |
| 46 | } |
| 47 | const lines = r.stdout.split('\n').filter(Boolean); |
| 48 | if (lines.length === 0) { |
| 49 | return { content: '[NO_COMMITS] No commits match those filters.' }; |
| 50 | } |
| 51 | const formatted = lines.map(l => { |
| 52 | const [sha, date, author, ...rest] = l.split('\x00'); |
| 53 | const subject = rest.join('\x00'); // subject can contain NUL if anyone embedded one; very unlikely |
| 54 | return `${sha} ${date} ${author?.padEnd(20).slice(0, 20)} ${subject}`; |
| 55 | }); |
| 56 | return { |
| 57 | content: `${lines.length} commit${lines.length > 1 ? 's' : ''}:\n${formatted.join('\n')}`, |
| 58 | metadata: { count: lines.length }, |
| 59 | }; |
| 60 | } |
| 61 | } |