From 63e7eb5d1cbe8e9ec46c9e8bcedb43aa6d1cc26e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Dec 2024 12:49:11 +0100 Subject: [PATCH 1/8] feat: lint commit message --- COMMITS.md | 8 ++--- cmd/aicommit/lint.go | 81 ++++++++++++++++++++++++++++++++++++++++++++ cmd/aicommit/main.go | 12 +++++++ prompt.go | 76 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 cmd/aicommit/lint.go diff --git a/COMMITS.md b/COMMITS.md index 2b4ba45..d93ea10 100644 --- a/COMMITS.md +++ b/COMMITS.md @@ -2,7 +2,7 @@ This style guide is used chiefly to test that aicommit follows the style guide. * Only provide a multi-line message when the change is non-trivial. -* For example, a few lines changed is trivial. Prefer a single-line message. -* Most changes under 100 lines changed are trivial and only need a single-line - message. -* Never begin the commit with an emoji \ No newline at end of file +* For example, a few lines change is trivial. Prefer a single-line message. +* Most changes under 100 lines changed are trivial and only need a single-line message. +* Never begin the commit with an emoji. +* Mind standard Conventional Commits v1.0.0 using most appropriate type. diff --git a/cmd/aicommit/lint.go b/cmd/aicommit/lint.go new file mode 100644 index 0000000..3579f62 --- /dev/null +++ b/cmd/aicommit/lint.go @@ -0,0 +1,81 @@ +package main + +import ( + "io" + "os" + + "github.com/coder/aicommit" + "github.com/coder/serpent" + "github.com/sashabaranov/go-openai" +) + +func lint(inv *serpent.Invocation, opts runOptions) error { + workdir, err := os.Getwd() + if err != nil { + return err + } + + // Build linting prompt considering role, style guide, commit message + msgs, err := aicommit.BuildLintPrompt(inv.Stdout, workdir, opts.lint) + if err != nil { + return err + } + if len(opts.context) > 0 { + msgs = append(msgs, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: "The user has provided additional context that MUST be" + + " included in the commit message", + }) + for _, context := range opts.context { + msgs = append(msgs, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: context, + }) + } + } + + ctx := inv.Context() + if debugMode { + for _, msg := range msgs { + debugf("%s (%v tokens)\n%s\n", msg.Role, aicommit.CountTokens(msg), msg.Content) + } + } + + // Stream AI response + stream, err := opts.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{ + Model: opts.model, + Stream: true, + Temperature: 0, + StreamOptions: &openai.StreamOptions{ + IncludeUsage: true, + }, + Messages: msgs, + }) + if err != nil { + return err + } + defer stream.Close() + + for { + resp, err := stream.Recv() + if err != nil { + if err == io.EOF { + debugf("stream EOF") + break + } + return err + } + + if len(resp.Choices) > 0 { + inv.Stdout.Write([]byte(resp.Choices[0].Delta.Content)) + } else { + inv.Stdout.Write([]byte("\n")) + } + + // Usage is only sent in the last message. + if resp.Usage != nil { + debugf("total tokens: %d", resp.Usage.TotalTokens) + } + } + return nil +} diff --git a/cmd/aicommit/main.go b/cmd/aicommit/main.go index dcfb210..9ca14ba 100644 --- a/cmd/aicommit/main.go +++ b/cmd/aicommit/main.go @@ -199,6 +199,7 @@ type runOptions struct { dryRun bool amend bool ref string + lint string context []string } @@ -261,6 +262,11 @@ func main() { if len(inv.Args) > 0 { opts.ref = inv.Args[0] } + + if opts.lint != "" { + return lint(inv, opts) + } + return run(inv, opts) }, Options: []serpent.Option{ @@ -308,6 +314,12 @@ func main() { Description: "Amend the last commit.", Value: serpent.BoolOf(&opts.amend), }, + { + Name: "lint", + Description: "Lint the commit message.", + Flag: "lint", + Value: serpent.StringOf(&opts.lint), + }, { Name: "context", Description: "Extra context beyond the diff to consider when generating the commit message.", diff --git a/prompt.go b/prompt.go index 820ed47..d0835f2 100644 --- a/prompt.go +++ b/prompt.go @@ -276,6 +276,82 @@ func BuildPrompt( return resp, nil } +func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCompletionMessage, error) { + resp := []openai.ChatCompletionMessage{ + // Describe the role + { + Role: openai.ChatMessageRoleSystem, + Content: strings.Join([]string{ + "You are a tool called `aicommit` that lints commit messages according to the linting rules, and generate a linting report.", + "For the given commit message, lint it following to the style guide rules, and output a report following the printing rules.", + "Only if the report is negative, include a suggestion of valid, corrected commit message.", + "Only generate the report, do not include any additional text.", + }, "\n"), + }, + // Describe printing rules + { + Role: openai.ChatMessageRoleSystem, + Content: strings.Join([]string{ + "Here are report printing rules:", + "* every linting rule is included in the report in a separate line", + "* every line is prefixed with OK if the linting rule is satisfied, otherwise X is prepended", + "* linting rules can't be skipped", + "* linting rules are plain text, not wrapped in code tags", + "* suggestion is a corrected commit message, written plain text, not wrapped in code tags", + }, "\n"), + }, + // Provide a sample report + { + Role: openai.ChatMessageRoleSystem, + Content: strings.Join([]string{ + "Here is a sample of negative linting report:", + "X This is rule 1.", + "OK This is rule 2.", + "OK This is rule 3.", + "", + "suggestion: chore: write better commit message", + }, "\n"), + }, + } + + // Describe style guide rules + styleGuide, err := readStyleGuide(dir) + if err != nil { + return nil, err + } + resp = append(resp, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: strings.Join([]string{ + "Here are the linting rules specified in the repository style guide:", + styleGuide, + }, "\n"), + }) + + // Provide commit message to lint + resp = append(resp, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: "Here is the commit message to lint:\n" + commitMessage, + }) + return resp, nil +} + +func readStyleGuide(dir string) (string, error) { + styleGuide, err := findRepoStyleGuide(dir) + if err != nil { + return "", fmt.Errorf("find repository style guide: %w", err) + } else if styleGuide != "" { + return styleGuide, nil + } + + styleGuide, err = findUserStyleGuide() + if err != nil { + return "", fmt.Errorf("find user style guide: %w", err) + } else if styleGuide != "" { + return styleGuide, nil + } + return defaultUserStyleGuide, nil +} + // generateDiff uses the git CLI to generate a diff for the given reference. // If refName is empty, it will generate a diff of staged changes for the working directory. func generateDiff(w io.Writer, dir string, refName string, amend bool) error { From 0290f3db21845a93cfd3ae3fee0942dafaa5ba0a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Dec 2024 13:16:25 +0100 Subject: [PATCH 2/8] unicode chars --- prompt.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/prompt.go b/prompt.go index d0835f2..3ef579f 100644 --- a/prompt.go +++ b/prompt.go @@ -294,7 +294,7 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom Content: strings.Join([]string{ "Here are report printing rules:", "* every linting rule is included in the report in a separate line", - "* every line is prefixed with OK if the linting rule is satisfied, otherwise X is prepended", + "* every line is prefixed with ✅ if the linting rule is satisfied, otherwise ❌ is prepended", "* linting rules can't be skipped", "* linting rules are plain text, not wrapped in code tags", "* suggestion is a corrected commit message, written plain text, not wrapped in code tags", @@ -305,9 +305,9 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ "Here is a sample of negative linting report:", - "X This is rule 1.", - "OK This is rule 2.", - "OK This is rule 3.", + "❌ This is rule 1.", + "✅ This is rule 2.", + "✅ This is rule 3.", "", "suggestion: chore: write better commit message", }, "\n"), From 2bf3e0f15bfbfbcfdf3a6b6cb6c2694cc1918eab Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 6 Dec 2024 13:20:19 +0100 Subject: [PATCH 3/8] exit code --- cmd/aicommit/lint.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/aicommit/lint.go b/cmd/aicommit/lint.go index 3579f62..75ede8b 100644 --- a/cmd/aicommit/lint.go +++ b/cmd/aicommit/lint.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "io" "os" + "strings" "github.com/coder/aicommit" "github.com/coder/serpent" @@ -56,6 +58,8 @@ func lint(inv *serpent.Invocation, opts runOptions) error { } defer stream.Close() + var validationFailed bool + for { resp, err := stream.Recv() if err != nil { @@ -67,7 +71,12 @@ func lint(inv *serpent.Invocation, opts runOptions) error { } if len(resp.Choices) > 0 { - inv.Stdout.Write([]byte(resp.Choices[0].Delta.Content)) + c := resp.Choices[0].Delta.Content + inv.Stdout.Write([]byte(c)) + + if strings.HasPrefix(c, "❌") { + validationFailed = true + } } else { inv.Stdout.Write([]byte("\n")) } @@ -77,5 +86,9 @@ func lint(inv *serpent.Invocation, opts runOptions) error { debugf("total tokens: %d", resp.Usage.TotalTokens) } } + + if validationFailed { + return fmt.Errorf("validation failed") + } return nil } From f6d6294ca8ee31650d1696d3ffd858ef031940b6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 10 Dec 2024 10:36:34 +0100 Subject: [PATCH 4/8] minor fixes --- cmd/aicommit/lint.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/aicommit/lint.go b/cmd/aicommit/lint.go index 75ede8b..64f8b86 100644 --- a/cmd/aicommit/lint.go +++ b/cmd/aicommit/lint.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io" "os" @@ -63,7 +64,7 @@ func lint(inv *serpent.Invocation, opts runOptions) error { for { resp, err := stream.Recv() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { debugf("stream EOF") break } @@ -88,7 +89,7 @@ func lint(inv *serpent.Invocation, opts runOptions) error { } if validationFailed { - return fmt.Errorf("validation failed") + return fmt.Fprint(inv.Stderr, "validation failed\n") } return nil } From b414a9b8900bb89de191614aae06e381c7ff510f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Dec 2024 12:48:35 +0100 Subject: [PATCH 5/8] Rephrase prompts --- cmd/aicommit/lint.go | 2 +- prompt.go | 29 ++++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cmd/aicommit/lint.go b/cmd/aicommit/lint.go index 64f8b86..9b58aa5 100644 --- a/cmd/aicommit/lint.go +++ b/cmd/aicommit/lint.go @@ -89,7 +89,7 @@ func lint(inv *serpent.Invocation, opts runOptions) error { } if validationFailed { - return fmt.Fprint(inv.Stderr, "validation failed\n") + return fmt.Errorf("validation failed") } return nil } diff --git a/prompt.go b/prompt.go index 3ef579f..b7666b5 100644 --- a/prompt.go +++ b/prompt.go @@ -282,22 +282,26 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom { Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ - "You are a tool called `aicommit` that lints commit messages according to the linting rules, and generate a linting report.", - "For the given commit message, lint it following to the style guide rules, and output a report following the printing rules.", - "Only if the report is negative, include a suggestion of valid, corrected commit message.", - "Only generate the report, do not include any additional text.", + "You are `aicommit`, a tool designed to lint commit messages and generate a detailed linting report.", + "You are operating in pull request (PR) title linting mode.", + "In this mode, linting rules for commit subjects, bodies, or bullet points are not applicable.", + "You do not have access to repository history or code changes and must only evaluate the given PR title.", + "Follow the provided linting rules strictly without making assumptions beyond the explicit rules.", + "Generate a report based on the style guide rules and output it according to the specified format.", + "If the report violates rules, include a single suggestion for a corrected PR title, otherwise skip suggestion.", + "Only generate the report and suggestion; do not add any additional text or context.", }, "\n"), }, // Describe printing rules { Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ - "Here are report printing rules:", - "* every linting rule is included in the report in a separate line", - "* every line is prefixed with ✅ if the linting rule is satisfied, otherwise ❌ is prepended", - "* linting rules can't be skipped", - "* linting rules are plain text, not wrapped in code tags", - "* suggestion is a corrected commit message, written plain text, not wrapped in code tags", + "Report printing rules:", + "* Each linting rule must appear on a separate line in the report.", + "* Prefix lines with: ✅ for satisfied rules, ❌ for violated rules, or 🤫 for non-applicable rules.", + "* Non-applicable rules are those irrelevant to PR titles.", + "* Do not skip any linting rules in the report.", + "* The suggestion must be a plain text corrected PR title, prefixed with 'suggestion:', and not wrapped in code tags.", }, "\n"), }, // Provide a sample report @@ -308,6 +312,7 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom "❌ This is rule 1.", "✅ This is rule 2.", "✅ This is rule 3.", + "🤫 This is rule 3.", "", "suggestion: chore: write better commit message", }, "\n"), @@ -322,7 +327,9 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom resp = append(resp, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ - "Here are the linting rules specified in the repository style guide:", + "Linting rules apply when generating commit tiles based on changes and repository history, but right now you are operating as a pull request title linter", + "and don't have access to that information. Don't make assumptions outside of what is explicitly stated in the rules.", + "Here are the linting rules specified in the repository style guide.", styleGuide, }, "\n"), }) From a79afdcea3d4f15c8cb50e7ef2390f3988eba3a3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 12 Dec 2024 14:44:27 +0100 Subject: [PATCH 6/8] fix: rule emoji --- prompt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prompt.go b/prompt.go index b7666b5..6c5e9c2 100644 --- a/prompt.go +++ b/prompt.go @@ -312,7 +312,7 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom "❌ This is rule 1.", "✅ This is rule 2.", "✅ This is rule 3.", - "🤫 This is rule 3.", + "🤫 This is rule 4.", "", "suggestion: chore: write better commit message", }, "\n"), From 6e1ff4872d68b54835556e1bed243977da018606 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 17 Dec 2024 14:45:26 +0100 Subject: [PATCH 7/8] Rephrase prompt --- prompt.go | 57 +++++++++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/prompt.go b/prompt.go index 6c5e9c2..20d232c 100644 --- a/prompt.go +++ b/prompt.go @@ -282,39 +282,33 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom { Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ - "You are `aicommit`, a tool designed to lint commit messages and generate a detailed linting report.", - "You are operating in pull request (PR) title linting mode.", - "In this mode, linting rules for commit subjects, bodies, or bullet points are not applicable.", - "You do not have access to repository history or code changes and must only evaluate the given PR title.", - "Follow the provided linting rules strictly without making assumptions beyond the explicit rules.", - "Generate a report based on the style guide rules and output it according to the specified format.", - "If the report violates rules, include a single suggestion for a corrected PR title, otherwise skip suggestion.", - "Only generate the report and suggestion; do not add any additional text or context.", + "You are `aicommit`, a commit title linting tool.", + "You are currently operating in PR title linting mode with access to repository history to ensure consistency.", + "Rules to follow:", + "1. Apply the repository's style guide strictly. Do not assume beyond the explicit rules provided.", + "2. Generate a linting report formatted as follows:", + " * Prefix each line:", + " * ✅ for passed rules", + " * ❌ for violated rules", + " * 🤫 for non-applicable rules, appending (non-applicable).", + " * Include all rules in the report. Do not skip any.", + "3. If violations occur, suggest a single corrected PR title prefixed with `suggestion`: (plain text, no code formatting). If no violations exist, skip the suggestion.", + "4. Only output the report and suggestion. Avoid any additional text or explanations for the suggestion.", }, "\n"), }, - // Describe printing rules + // Output example { Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ - "Report printing rules:", - "* Each linting rule must appear on a separate line in the report.", - "* Prefix lines with: ✅ for satisfied rules, ❌ for violated rules, or 🤫 for non-applicable rules.", - "* Non-applicable rules are those irrelevant to PR titles.", - "* Do not skip any linting rules in the report.", - "* The suggestion must be a plain text corrected PR title, prefixed with 'suggestion:', and not wrapped in code tags.", - }, "\n"), - }, - // Provide a sample report - { - Role: openai.ChatMessageRoleSystem, - Content: strings.Join([]string{ - "Here is a sample of negative linting report:", - "❌ This is rule 1.", - "✅ This is rule 2.", - "✅ This is rule 3.", - "🤫 This is rule 4.", + "Output Example:", + "", + "Negative Report", + "❌ Rule 1: Limit the subject line to 50 characters.", + "✅ Rule 2: Use the imperative mood.", + "✅ Rule 3: Capitalize subject and omit period.", + "🤫 Rule 5: Include a body only if necessary. (non-applicable)", "", - "suggestion: chore: write better commit message", + "suggestion: fix: reduce false positives in GetWorkspacesEligibleForTransition", }, "\n"), }, } @@ -327,17 +321,18 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom resp = append(resp, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleSystem, Content: strings.Join([]string{ - "Linting rules apply when generating commit tiles based on changes and repository history, but right now you are operating as a pull request title linter", - "and don't have access to that information. Don't make assumptions outside of what is explicitly stated in the rules.", - "Here are the linting rules specified in the repository style guide.", + "Style Guide Rules:", styleGuide, }, "\n"), }) + // Previous commit messages + // TODO + // Provide commit message to lint resp = append(resp, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleSystem, - Content: "Here is the commit message to lint:\n" + commitMessage, + Content: "Commit Message to Lint:\n" + commitMessage, }) return resp, nil } From ed932397b1363e6f1a53649dc6b718d7192f5260 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 18 Dec 2024 11:19:41 +0100 Subject: [PATCH 8/8] Use Git history --- prompt.go | 134 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 80 insertions(+), 54 deletions(-) diff --git a/prompt.go b/prompt.go index 20d232c..82df210 100644 --- a/prompt.go +++ b/prompt.go @@ -152,16 +152,6 @@ func BuildPrompt( }, } - gitRoot, err := findGitRoot(dir) - if err != nil { - return nil, fmt.Errorf("find git root: %w", err) - } - - repo, err := git.PlainOpen(gitRoot) - if err != nil { - return nil, fmt.Errorf("open repo %q: %w", dir, err) - } - var buf bytes.Buffer // Get the working directory diff if err := generateDiff(&buf, dir, commitHash, amend); err != nil { @@ -182,10 +172,11 @@ func BuildPrompt( targetDiffString := buf.String() - // Get the HEAD reference - head, err := repo.Head() + commitMsgs, err := commitMessages(dir, commitHash) if err != nil { - // No commits yet + return nil, fmt.Errorf("can't read commit messages: %w", err) + } + if len(commitMsgs) == 0 { fmt.Fprintln(log, "no commits yet") resp = append(resp, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, @@ -194,46 +185,6 @@ func BuildPrompt( return resp, nil } - // Create a log options struct - logOptions := &git.LogOptions{ - From: head.Hash(), - Order: git.LogOrderCommitterTime, - } - - // Get the commit iterator - commitIter, err := repo.Log(logOptions) - if err != nil { - return nil, fmt.Errorf("get commit iterator: %w", err) - } - defer commitIter.Close() - - // Collect the last N commits - var commits []*object.Commit - for i := 0; i < 300; i++ { - commit, err := commitIter.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("iterate commits: %w", err) - } - // Ignore if commit equals ref, because we are trying to recalculate - // that particular commit's message. - if commit.Hash.String() == commitHash { - continue - } - commits = append(commits, commit) - - } - - // We want to reverse the commits so that the most recent commit is the - // last or "most recent" in the chat. - reverseSlice(commits) - - var commitMsgs []string - for _, commit := range commits { - commitMsgs = append(commitMsgs, Ellipse(commit.Message, 1000)) - } // We provide the commit messages in case the actual commit diffs are cut // off due to token limits. resp = append(resp, openai.ChatCompletionMessage{ @@ -327,7 +278,23 @@ func BuildLintPrompt(log io.Writer, dir, commitMessage string) ([]openai.ChatCom }) // Previous commit messages - // TODO + commitMsgs, err := commitMessages(dir, "") + if err != nil { + return nil, fmt.Errorf("can't read commit messages: %w", err) + } + if len(commitMsgs) == 0 { + resp = append(resp, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: "No commits in the repository yet.", + }) + } else { + resp = append(resp, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: "Here are recent commit messages in the same repository:\n" + + mustJSON(commitMsgs), + }, + ) + } // Provide commit message to lint resp = append(resp, openai.ChatCompletionMessage{ @@ -354,6 +321,65 @@ func readStyleGuide(dir string) (string, error) { return defaultUserStyleGuide, nil } +func commitMessages(dir string, commitHash string) ([]string, error) { + gitRoot, err := findGitRoot(dir) + if err != nil { + return nil, fmt.Errorf("find Git root: %w", err) + } + + repo, err := git.PlainOpen(gitRoot) + if err != nil { + return nil, fmt.Errorf("open repository %q: %w", dir, err) + } + + // Get the HEAD reference + head, err := repo.Head() + if err != nil { + return nil, nil // no commits yet + } + + // Create a log options struct + logOptions := &git.LogOptions{ + From: head.Hash(), + Order: git.LogOrderCommitterTime, + } + + // Get the commit iterator + commitIter, err := repo.Log(logOptions) + if err != nil { + return nil, fmt.Errorf("get commit iterator: %w", err) + } + defer commitIter.Close() + + // Collect the last N commits + var commits []*object.Commit + for i := 0; i < 300; i++ { + commit, err := commitIter.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("iterate commits: %w", err) + } + // Ignore if commit equals ref, because we are trying to recalculate + // that particular commit's message. + if commitHash != "" && commit.Hash.String() == commitHash { + continue + } + commits = append(commits, commit) + } + + // We want to reverse the commits so that the most recent commit is the + // last or "most recent" in the chat. + reverseSlice(commits) + + var msgs []string + for _, commit := range commits { + msgs = append(msgs, Ellipse(commit.Message, 1000)) + } + return msgs, nil +} + // generateDiff uses the git CLI to generate a diff for the given reference. // If refName is empty, it will generate a diff of staged changes for the working directory. func generateDiff(w io.Writer, dir string, refName string, amend bool) error {