Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions src/Glyph/Commands/CommitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@ public static class CommitCommand
{
public static Command Create()
{
var messageArg = new Argument<string>("message") { Description = "Commit message" };
var messageArg = new Argument<string?>("message") { Description = "Commit message", DefaultValueFactory = _ => null };
var amendOption = new Option<bool>("--amend") { Description = "Amend the previous commit" };
var allOption = new Option<bool>("-A") { Description = "Stage all changes before committing" };
var aiOption = new Option<bool>("--ai") { Description = "Generate commit message using AI" };

var command = new Command("commit") { Description = "Create a git commit" };
command.Arguments.Add(messageArg);
command.Options.Add(amendOption);
command.Options.Add(allOption);
command.Options.Add(aiOption);

command.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var message = parseResult.GetValue(messageArg)!;
var message = parseResult.GetValue(messageArg);
var amend = parseResult.GetValue(amendOption);
var addAll = parseResult.GetValue(allOption);
var useAi = parseResult.GetValue(aiOption);

if (addAll)
{
Expand All @@ -35,6 +38,27 @@ public static Command Create()
}
}

// Resolve commit message if not provided
if (string.IsNullOrEmpty(message))
{
if (useAi)
{
message = await GenerateCommitMessageWithAi();
if (message == null)
return;
}
else
{
message = AnsiConsole.Prompt(
new TextPrompt<string>("Enter commit message:"));
if (string.IsNullOrWhiteSpace(message))
{
AnsiConsole.MarkupLine("[red]Commit message cannot be empty.[/]");
return;
}
}
}

var escapedMessage = message.Replace("\"", "\\\"");
var args = amend
? $"commit --amend -m \"{escapedMessage}\""
Expand All @@ -60,4 +84,55 @@ public static Command Create()

return command;
}

internal static async Task<string?> GenerateCommitMessageWithAi()
{
var (diffExit, diff, diffErr) = await ProcessRunner.RunAsync("git", "diff --staged");
if (diffExit != 0 || string.IsNullOrWhiteSpace(diff))
{
// Try unstaged diff as fallback (for ship command which stages after)
(diffExit, diff, diffErr) = await ProcessRunner.RunAsync("git", "diff");
if (diffExit != 0 || string.IsNullOrWhiteSpace(diff))
{
AnsiConsole.MarkupLine("[yellow]No changes found to generate a message from.[/]");
return null;
}
}

string? message = null;
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.StartAsync("Generating commit message...", async _ =>
{
try
{
message = await AiService.GenerateCommitMessageAsync(diff);
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]AI generation failed: {Markup.Escape(ex.Message)}[/]");
}
});

if (message == null)
{
AnsiConsole.MarkupLine("[red]Failed to generate commit message.[/]");
AnsiConsole.MarkupLine("[dim]Make sure GITHUB_TOKEN is set or 'gh' CLI is authenticated.[/]");
return null;
}

AnsiConsole.MarkupLine($"[bold]Generated message:[/] {Markup.Escape(message)}");
if (!AnsiConsole.Confirm("Use this message?", defaultValue: true))
{
message = AnsiConsole.Prompt(
new TextPrompt<string>("Enter commit message:"));
if (string.IsNullOrWhiteSpace(message))
{
AnsiConsole.MarkupLine("[red]Commit message cannot be empty.[/]");
return null;
}
}

return message;
}
}
75 changes: 72 additions & 3 deletions src/Glyph/Commands/PrCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ public static Command Create()
{
Description = "PR title (defaults to branch name)"
};
var aiOption = new Option<bool>("--ai") { Description = "Generate PR title and description using AI" };

var command = new Command("pr") { Description = "Create a pull request into the parent branch" };
command.Options.Add(titleOption);
command.Options.Add(aiOption);

command.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var title = parseResult.GetValue(titleOption);
var useAi = parseResult.GetValue(aiOption);
using var git = new GitService();
var current = git.CurrentBranchName;
var parent = git.GetParentBranch(current);
Expand All @@ -32,12 +35,37 @@ public static Command Create()
return;
}

// Create PR via gh CLI
string? body = null;

if (useAi && string.IsNullOrEmpty(title))
{
// Generate both title and body via AI
var generated = await GeneratePrWithAi(current, parent);
if (generated != null)
{
title = generated.Value.Title;
body = generated.Value.Body;
}
}

// Fall back to defaults
var prTitle = title ?? current.Replace("-", " ").Replace("/", ": ");

AnsiConsole.MarkupLine($"Creating PR: [bold]{Markup.Escape(prTitle)}[/] -> [blue]{parent}[/]");

var (exitCode, output, error) = await ProcessRunner.RunAsync(
"gh", $"pr create --base {parent} --title \"{prTitle}\" --fill");
var escapedTitle = prTitle.Replace("\"", "\\\"");
string ghArgs;
if (!string.IsNullOrEmpty(body))
{
var escapedBody = body.Replace("\"", "\\\"");
ghArgs = $"pr create --base {parent} --title \"{escapedTitle}\" --body \"{escapedBody}\"";
}
else
{
ghArgs = $"pr create --base {parent} --title \"{escapedTitle}\" --fill";
}

var (exitCode, output, error) = await ProcessRunner.RunAsync("gh", ghArgs);

if (exitCode == 0)
{
Expand All @@ -55,4 +83,45 @@ public static Command Create()

return command;
}

private static async Task<(string Title, string Body)?> GeneratePrWithAi(string branchName, string parentBranch)
{
var (diffExit, diff, _) = await ProcessRunner.RunAsync("git", $"diff {parentBranch}...HEAD");
if (diffExit != 0 || string.IsNullOrWhiteSpace(diff))
{
AnsiConsole.MarkupLine("[yellow]No changes found between branch and parent.[/]");
return null;
}

(string Title, string Body)? result = null;
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.StartAsync("Generating PR description...", async _ =>
{
try
{
result = await AiService.GeneratePrDescriptionAsync(diff, branchName, parentBranch);
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]AI generation failed: {Markup.Escape(ex.Message)}[/]");
}
});

if (result == null)
{
AnsiConsole.MarkupLine("[red]Failed to generate PR description.[/]");
AnsiConsole.MarkupLine("[dim]Make sure GITHUB_TOKEN is set or 'gh' CLI is authenticated.[/]");
return null;
}

AnsiConsole.MarkupLine($"[bold]Generated title:[/] {Markup.Escape(result.Value.Title)}");
AnsiConsole.MarkupLine("[bold]Generated body:[/]");
AnsiConsole.WriteLine(result.Value.Body);

if (!AnsiConsole.Confirm("Use this PR description?", defaultValue: true))
return null;

return result;
}
}
28 changes: 26 additions & 2 deletions src/Glyph/Commands/ShipCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ public static class ShipCommand
{
public static Command Create()
{
var messageArg = new Argument<string>("message") { Description = "Commit message" };
var messageArg = new Argument<string?>("message") { Description = "Commit message", DefaultValueFactory = _ => null };
var aiOption = new Option<bool>("--ai") { Description = "Generate commit message using AI" };

var command = new Command("ship") { Description = "Stage all changes, commit, and push to origin" };
command.Arguments.Add(messageArg);
command.Options.Add(aiOption);

command.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var message = parseResult.GetValue(messageArg)!;
var message = parseResult.GetValue(messageArg);
var useAi = parseResult.GetValue(aiOption);
using var git = new GitService();
var current = git.CurrentBranchName;

Expand All @@ -29,6 +32,27 @@ public static Command Create()
return;
}

// Resolve commit message if not provided
if (string.IsNullOrEmpty(message))
{
if (useAi)
{
message = await CommitCommand.GenerateCommitMessageWithAi();
if (message == null)
return;
}
else
{
message = AnsiConsole.Prompt(
new TextPrompt<string>("Enter commit message:"));
if (string.IsNullOrWhiteSpace(message))
{
AnsiConsole.MarkupLine("[red]Commit message cannot be empty.[/]");
return;
}
}
}

// Commit
var escapedMessage = message.Replace("\"", "\\\"");
var (commitExit, commitOut, commitErr) = await ProcessRunner.RunAsync("git", $"commit -m \"{escapedMessage}\"");
Expand Down
Loading