From 6f43412a6a76ef4b8ff59d3abda6304032a5623b Mon Sep 17 00:00:00 2001 From: rick Date: Thu, 30 May 2024 15:46:37 +0800 Subject: [PATCH] commit: Add AI-powered commit message generation Go commit message: ``` Add AI-powered commit message generation This commit adds AI-powered commit message generation using the OneAPI server. The commit message is generated using the OneAPI token and the AI_PROVIDER environment variable. Features: - AI-powered commit message generation - Supports OneAPI server Bugs fixed: - None Refs: - OneAPI: https://github.com/songquanpeng/one-api ``` --- README.md | 11 ++++ cmd/msg.go | 127 ++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 2 +- go.mod | 9 ++-- go.sum | 11 ++-- go.work.sum | 2 + pkg/oneapi/types.go | 32 +++++++++++ 7 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 cmd/msg.go create mode 100644 pkg/oneapi/types.go diff --git a/README.md b/README.md index 1c81675..fe6390d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,17 @@ ## Usage +### Generate commit message with AI + +```shell +export AI_PROVIDER=your-one-api-server-address +export ONEAPI_TOKEN=your-one-api-token + +gogit commit +``` + +It supports [one-api](https://github.com/songquanpeng/one-api) only. + ### Checkout to branch or PR Ideally, `gogit` could checkout to your branch or PR in any kind of git repository. diff --git a/cmd/msg.go b/cmd/msg.go new file mode 100644 index 0000000..fb33800 --- /dev/null +++ b/cmd/msg.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "syscall" + + fakeruntime "github.com/linuxsuren/go-fake-runtime" + "github.com/linuxsuren/gogit/pkg/oneapi" + "github.com/spf13/cobra" + "io" + "net/http" +) + +type commitOption struct { + provider string + token string + flags []string + runtime fakeruntime.Execer +} + +func newCommitCmd() *cobra.Command { + opt := &commitOption{ + runtime: fakeruntime.NewDefaultExecer(), + } + cmd := &cobra.Command{ + Use: "commit", + RunE: opt.runE, + PreRunE: opt.preRunE, + Short: "Commit the current changes with AI", + Long: `Commit the current changes with AI. +The AI provider is defined by the environment variable AI_PROVIDER, +and the token is defined by the environment variable ONEAPI_TOKEN.`, + } + cmd.Flags().StringSliceVarP(&opt.flags, "flag", "", []string{}, "The flags of the git commit command") + return cmd +} + +func (o *commitOption) preRunE(cmd *cobra.Command, args []string) (err error) { + o.provider = os.Getenv("AI_PROVIDER") + if o.provider == "" { + err = fmt.Errorf("AI_PROVIDER is not set") + } + o.token = os.Getenv("ONEAPI_TOKEN") + if o.token == "" { + err = errors.Join(err, fmt.Errorf("ONEAPI_TOKEN is not set")) + } + return +} + +func (o *commitOption) runE(cmd *cobra.Command, args []string) (err error) { + var gitdiff string + gitdiff, err = o.getGitDiff() + + payload := oneapi.NewChatPayload(fmt.Sprintf("Please write a git commit message for the following git diff:\n%s", gitdiff), "chatglm_std") + + var body []byte + if body, err = json.Marshal(payload); err != nil { + return + } + + var req *http.Request + req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/chat/completions", o.provider), io.NopCloser(bytes.NewReader(body))) + if err != nil { + return + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", o.token)) + req.Header.Set("Content-Type", "application/json") + + var resp *http.Response + resp, err = http.DefaultClient.Do(req) + if err != nil { + return + } + + if resp.StatusCode == http.StatusOK { + // read the body and parse to oenapi.ChatResponse + var chatResp oneapi.ChatResponse + if err = json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { + return + } + + var tempF *os.File + if tempF, err = os.CreateTemp(os.TempDir(), "msg"); err != nil { + return + } + + content := chatResp.Choices[0].Message.Content + // convert \n to new line + content = strings.ReplaceAll(content, "\\n", "\n") + if _, err = io.WriteString(tempF, content); err != nil { + return + } + if err = tempF.Close(); err != nil { + return + } + + cmd.Println("start to commit with", tempF.Name()) + + var gitExe string + if gitExe, err = o.runtime.LookPath("git"); err != nil { + return + } + + if err = o.runtime.RunCommand(gitExe, "add", "."); err != nil { + return + } + + opts := []string{"git", "commit", "--edit", "--file", tempF.Name()} + for _, flag := range o.flags { + opts = append(opts, flag) + } + + err = syscall.Exec(gitExe, opts, append(os.Environ(), "GIT_EDITOR=vim")) + } + return +} + +func (o *commitOption) getGitDiff() (diff string, err error) { + // run command git diff and get the output + diff, err = o.runtime.RunCommandAndReturn("git", ".", "diff") + return +} diff --git a/cmd/root.go b/cmd/root.go index 436830b..ef6d28f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,6 @@ func NewRootCommand() (c *cobra.Command) { c.AddCommand(newCheckoutCommand(), newStatusCmd(), newCommentCommand(), - newPullRequestCmd()) + newPullRequestCmd(), newCommitCmd()) return } diff --git a/go.mod b/go.mod index c7165d0..ba59496 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/linuxsuren/gogit go 1.18 require ( - github.com/h2non/gock v1.2.0 github.com/jenkins-x/go-scm v1.11.19 + github.com/linuxsuren/go-fake-runtime v0.0.4 + github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.6.1 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 ) require ( @@ -17,11 +18,9 @@ require ( github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.3.1 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect @@ -38,7 +37,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260 // indirect github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect diff --git a/go.sum b/go.sum index 9a46245..021871b 100644 --- a/go.sum +++ b/go.sum @@ -89,10 +89,7 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -124,6 +121,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/linuxsuren/go-fake-runtime v0.0.4 h1:y+tvBuw6MKTCav8Bo5HWwaXhBx1Z//VAvqI3gpOWqvw= +github.com/linuxsuren/go-fake-runtime v0.0.4/go.mod h1:zmh6J78hSnWZo68faMA2eKOdaEp8eFbERHi3ZB9xHCQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= @@ -142,8 +141,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -187,8 +184,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo= github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w= diff --git a/go.work.sum b/go.work.sum index 319a58f..04df4f9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -453,6 +453,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaW github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -556,6 +557,7 @@ github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= diff --git a/pkg/oneapi/types.go b/pkg/oneapi/types.go new file mode 100644 index 0000000..478b9d7 --- /dev/null +++ b/pkg/oneapi/types.go @@ -0,0 +1,32 @@ +package oneapi + +type ChatMessage struct { + Content string `json:"content"` + Role string `json:"role"` +} + +type ChatPayload struct { + Messages []ChatMessage `json:"messages"` + Model string `json:"model"` +} + +type ChatResponse struct { + Created int64 `json:"created"` + ID string `json:"id"` + Object string `json:"object"` + Usage map[string]int `json:"usage"` + Choices []ChatResponseChoice `json:"choices"` +} + +type ChatResponseChoice struct { + FinishReason string `json:"finish_reason"` + Index int `json:"index"` + Message ChatMessage `json:"message"` +} + +func NewChatPayload(message string, model string) ChatPayload { + return ChatPayload{ + Messages: []ChatMessage{{Content: message, Role: "user"}}, + Model: model, + } +}