Skip to content

Instantly share code, notes, and snippets.

@vlazic
Last active January 6, 2023 04:44
Show Gist options
  • Save vlazic/1c80ec5f3f3d79402a0eff086bd67098 to your computer and use it in GitHub Desktop.
Save vlazic/1c80ec5f3f3d79402a0eff086bd67098 to your computer and use it in GitHub Desktop.
Suggests a commit message based on the staged files using OpenAI's GPT-3 API.
node_modules

Git Commit Message Suggestion Tool

This script uses the OpenAI GPT-C API to suggest semantic commit messages based on the diff of staged files in a Git repository, and allows the user to select and tweak the commit message before committing.

Prerequisites

  • Node.js
  • npm
  • Git
  • An OpenAI API key

Installation

  1. Clone this repository: git clone https://gist.github.com/1c80ec5f3f3d79402a0eff086bd67098.git suggest-commit-message
  2. Run npm install to install the dependencies.
  3. Make script globally available by running npm link.

Usage

  1. Stage the files that you want to commit.
  2. Run the script using OPENAI_API_KEY=ENTER_YOUR_KEY_HERE suggest-commit-message in any Git repository. Or create an alias for the command in your shell: alias scm='OPENAI_API_KEY=ENTER_YOUR_KEY_HERE suggest-commit-message'.
  3. Follow the prompts to select and optionally tweak the commit message.
  4. The staged files will be committed using the selected commit message.

Limitations

The script will throw an error if the diff of the staged files has more than 150 lines.

License

This project is licensed under the MIT License.

#!/usr/bin/env node
import { execSync } from "child_process";
import Enquirer from "enquirer";
const enquirer = new Enquirer();
import { Configuration, OpenAIApi } from "openai";
async function main() {
// throw an error if the OPENAI_API_KEY is not set
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is not set");
}
// Create an OpenAI API client
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
// Get the staged files
let diff = execSync("git diff --staged --name-only")
.toString()
.split("\n")
.filter(Boolean);
// Throw an error if there are no staged files
if (diff.length === 0) {
throw new Error("No staged files, nothing to commit");
}
diff = diff.join(" ");
// Get the diff for the staged files
const diffLines = execSync(
`git diff --staged --unified=0 ${diff}`,
).toString();
// Throw an error if there are more than allowed lines in the diff
const maxLines = 250;
if (diffLines.split("\n").length > maxLines) {
throw new Error(
`Too many lines in the diff: ${diffLines.split("\n").length
} lines, max allowed: ${maxLines}`,
);
}
// Use GPT-C to generate 5 commit messages based on the diff
const response = await openai.createCompletion({
model: "text-davinci-003",
prompt: `Please suggest 5 semantic commit messages (one of: feat, fix, chore, docs etc) based on the following git diff, do not prefix lines with numbers, mention files and changes, use more than 100 chars if needed:\n${diffLines}\n`,
temperature: 0.7,
max_tokens: 350,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
});
const choices = response.data.choices[0].text
.split("\n")
// remove the numbers from the beginning of the lines
.map((choice) => choice.trim().replace(/^\d+\.\s?/, ""))
.filter(Boolean);
await enquirer
.prompt([
{
type: "select",
name: "commitMessage",
message: "Please select a commit message:",
initial: 0,
choices,
},
{
type: "confirm",
name: "tweakMessage",
message: "Do you want to tweak the commit message before committing?",
},
])
.then(async (answers) => {
let commitMessage = answers.commitMessage;
if (answers.tweakMessage) {
// Prompt the user to enter a new commit message
return await enquirer
.prompt([
{
type: "input",
name: "commitMessage",
message: "Enter the commit message:",
// editable default message
initial: commitMessage,
},
])
.then((answers) => {
commitMessage = answers.commitMessage;
return commitMessage;
});
}
return commitMessage;
})
.then((commitMessage) => {
// Use the selected commit message to commit the staged files
const gitCommand = `git commit -m "${commitMessage}"`;
console.log(`Executing: ${gitCommand}`);
execSync(gitCommand);
});
}
try {
main();
} catch (error) {
// show the error message
console.error(error.message);
}
package main
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/PullRequestInc/go-gpt3"
"github.com/briandowns/spinner"
)
func isGitRepo() bool {
cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
_, err := cmd.Output()
if err != nil {
return false
}
return true
}
func getStagedFiles() []string {
// Get the staged files, ignore exit code of exec.Command
cmd := exec.Command("git", "diff", "--staged", "--name-only")
out, _ := cmd.Output()
staged := strings.Split(strings.TrimSpace(string(out)), "\n")
// if there are no staged files, return an empty slice
if staged[0] == "" {
return []string{}
}
return staged
}
func getDiff(stagedFiles []string) string {
cmd := exec.Command("git", "diff", "--staged", "--unified=0", strings.Join(stagedFiles, " "))
out, err := cmd.Output()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return string(out)
}
// return true if the diffLines are less than or equal to maxLines and also return the maxLines
func checkDiffLinesLength(diffLines string) (bool, int) {
maxLines := 150
return len(strings.Split(diffLines, "\n")) <= maxLines, maxLines
}
func generateCommitMessages(diffLines string, client gpt3.Client) string {
prompt := fmt.Sprintf("\n\n%s\n\nPlease suggest 5 semantic commit messages (prefixed with feat for features, fix for fixes, docs...) based on the above git diff, do not prefix lines with numbers, mention files and changes, use more than 100 chars in line if needed:\n", diffLines)
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
s.Start()
response, err := client.Completion(context.Background(), gpt3.CompletionRequest{
Prompt: []string{prompt},
MaxTokens: gpt3.IntPtr(350),
// Stop: []string{"."},
Echo: true,
})
s.Stop()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
choices := response.Choices[0].Text
// get rid of the prompt
choices = strings.ReplaceAll(choices, prompt, "")
// remove the numbers from the beginning of the lines
choices = strings.TrimSpace(strings.ReplaceAll(choices, "^\\d+\\.\\s?", ""))
// remove empty choices
choices = strings.TrimSpace(strings.ReplaceAll(choices, "\n\n", "\n"))
return choices
}
func selectCommitMessage(commitMessages string) string {
choicesArr := strings.Split(commitMessages, "\n")
var qs = []*survey.Question{
{
Name: "commitMessage",
Prompt: &survey.Select{
Message: "Please select a commit message:",
Options: choicesArr,
},
},
{
Name: "tweakMessage",
Prompt: &survey.Confirm{
Message: "Do you want to change the commit message before committing?",
},
},
}
answers := struct {
CommitMessage string
TweakMessage bool
}{}
err := survey.Ask(qs, &answers)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
commitMessage := answers.CommitMessage
if answers.TweakMessage {
prompt := &survey.Input{
Message: "Enter the commit message:",
Default: commitMessage,
}
err = survey.AskOne(prompt, &commitMessage)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
return commitMessage
}
func commitStagedFiles(commitMessage string) {
gitCommand := fmt.Sprintf("git commit -m %q", commitMessage)
fmt.Printf("Executing: %s\n", gitCommand)
cmd := exec.Command("git", "commit", "-m", commitMessage)
_, err := cmd.Output()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
// throw an error if the OPENAI_API_KEY is not set
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
fmt.Println("Error: OPENAI_API_KEY is not set")
os.Exit(1)
}
// Create an OpenAI API client
client := gpt3.NewClient(apiKey, gpt3.WithDefaultEngine(gpt3.TextDavinci003Engine))
// Check if command is executed in git repo
if !isGitRepo() {
fmt.Println("Error: Command must be executed in a git repository")
os.Exit(1)
}
// Get the staged files
stagedFiles := getStagedFiles()
if len(stagedFiles) == 0 {
fmt.Println("Error: No staged files, nothing to commit")
os.Exit(1)
}
// Get the diff for the staged files
diff := getDiff(stagedFiles)
// Throw an error if there are more than allowed lines in the diff
ok, maxLines := checkDiffLinesLength(diff)
if !ok {
fmt.Printf("Error: Diff lines are more than %d\n", maxLines)
os.Exit(1)
}
// Use GPT-3 to generate 5 commit messages based on the diff
commitMessages := generateCommitMessages(diff, client)
// Prompt the user to select a commit message
commitMessage := selectCommitMessage(commitMessages)
// Print the selected commit message
fmt.Printf("Commit message: %s\n", commitMessage)
// Use the selected commit message to commit the staged files
commitStagedFiles(commitMessage)
}
{
"name": "suggest-commit-message",
"version": "1.0.0",
"description": "Suggests a commit message based on the staged files using OpenAI's GPT-3 API.",
"main": "index.js",
"type": "module",
"author": "Vladimir Lazić <contact@vlazic.com> (https://vlazic.com/)",
"license": "MIT",
"dependencies": {
"enquirer": "2.3.6",
"openai": "3.1.0"
},
"bin": {
"suggest-commit-message": "index.js"
},
"volta": {
"node": "18.12.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment