├── .gitignore ├── tools.go ├── Makefile ├── pkg ├── gitgen │ ├── prompts │ │ ├── test.txt │ │ ├── commit-message.txt │ │ ├── code-review.txt │ │ └── test-scenario.txt │ ├── actiontype_string.go │ ├── config.go │ ├── prompt.go │ ├── register.go │ └── mod.go └── platforms │ ├── platforms.go │ ├── ollama.go │ ├── openai.go │ ├── gemini.go │ └── anthropic.go ├── .vscode └── launch.json ├── LICENSE ├── go.mod ├── cmd └── git-gen │ └── main.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /git-gen -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package main 5 | 6 | import ( 7 | _ "golang.org/x/tools/cmd/stringer" 8 | ) 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build install-tools 2 | 3 | build: 4 | @echo Code generation 5 | @go generate ./... 6 | @echo Building binary 7 | @go build ./cmd/git-gen/ 8 | 9 | download: 10 | @echo Download go.mod dependencies 11 | @go mod download 12 | 13 | install-tools: download 14 | @echo Installing tools from tools.go 15 | @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % 16 | -------------------------------------------------------------------------------- /pkg/gitgen/prompts/test.txt: -------------------------------------------------------------------------------- 1 | Please generate unit and integration tests based on the changes provided above, which are the output of a git diff command and attached files. The test scenarios should be comprehensive, covering all modifications, additions, and deletions in the code. All responses should use standard library's testing package, as these responses could be shared in a text-only terminal interface. 2 | -------------------------------------------------------------------------------- /pkg/gitgen/prompts/commit-message.txt: -------------------------------------------------------------------------------- 1 | You'll find a code snippet attached, which is the output of a git diff command. Please generate an efficient and concise commit message that highlights the key modifications made to the code. The commit message should adhere to the Conventional Commits convention, include most of the significant changes, and be suitable for text-only terminal environments. Avoid wrapping text, adding explanations, or providing directions—just output the commit message as a response. 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/cmd/git-gen/", 13 | "cwd": "${workspaceFolder}", 14 | "args": ["commit"] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /pkg/gitgen/prompts/code-review.txt: -------------------------------------------------------------------------------- 1 | You'll find a code snippet attached, which is the output of a git diff command. Please perform an efficient and concise code review that focuses exclusively on the newly added code, ignoring any removed sections. Highlight only the most crucial improvements that could be made to enhance the code quality, with an emphasis on common mistakes specific to the programming language and platform, rather than minor stylistic issues. The suggestions should follow best practices and be provided in a text-only, terminal-friendly markdown format. Do not include explanations for changes the author has already made; simply output the improvement suggestions as a response. 2 | -------------------------------------------------------------------------------- /pkg/gitgen/prompts/test-scenario.txt: -------------------------------------------------------------------------------- 1 | Please generate detailed test scenarios based on the changes provided above, which are the output of a git diff command and attached files. The test scenarios should be comprehensive, covering all modifications, additions, and deletions in the code. All responses should be formatted in markdown, as they will be shared in a text-only terminal interface. Ensure that each test scenario includes the following details: 2 | 3 | - Description: A brief summary of what the test scenario covers. 4 | - Steps: Detailed steps to execute the test scenario. 5 | - Expected Result: The anticipated outcome of the test scenario. 6 | - Actual Result: (To be filled out during testing.) 7 | -------------------------------------------------------------------------------- /pkg/gitgen/actiontype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ActionType"; DO NOT EDIT. 2 | 3 | package gitgen 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ActionCommitMessage-0] 12 | _ = x[ActionCodeReview-1] 13 | _ = x[ActionTestScenario-2] 14 | _ = x[ActionTest-3] 15 | } 16 | 17 | const _ActionType_name = "ActionCommitMessageActionCodeReviewActionTestScenarioActionTest" 18 | 19 | var _ActionType_index = [...]uint8{0, 19, 35, 53, 63} 20 | 21 | func (i ActionType) String() string { 22 | if i < 0 || i >= ActionType(len(_ActionType_index)-1) { 23 | return "ActionType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _ActionType_name[_ActionType_index[i]:_ActionType_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024-present Seyma Handekli. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/seymahandekli/git-gen 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | dario.cat/mergo v1.0.0 // indirect 9 | github.com/Microsoft/go-winio v0.6.1 // indirect 10 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 11 | github.com/cloudflare/circl v1.3.7 // indirect 12 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 13 | github.com/emirpasic/gods v1.18.1 // indirect 14 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 15 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 16 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 17 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 18 | github.com/kevinburke/ssh_config v1.2.0 // indirect 19 | github.com/pjbgf/sha1cd v0.3.0 // indirect 20 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 21 | github.com/skeema/knownhosts v1.2.2 // indirect 22 | github.com/xanzy/ssh-agent v0.3.3 // indirect 23 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 24 | golang.org/x/crypto v0.31.0 // indirect 25 | golang.org/x/mod v0.19.0 // indirect 26 | golang.org/x/net v0.33.0 // indirect 27 | golang.org/x/sync v0.10.0 // indirect 28 | golang.org/x/sys v0.28.0 // indirect 29 | gopkg.in/warnings.v0 v0.1.2 // indirect 30 | ) 31 | 32 | require ( 33 | github.com/go-git/go-git/v5 v5.12.0 34 | github.com/ollama/ollama v0.3.1 35 | github.com/urfave/cli/v3 v3.0.0-alpha9 36 | golang.org/x/tools v0.23.0 37 | ) 38 | -------------------------------------------------------------------------------- /pkg/platforms/platforms.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | type Platform string 10 | 11 | const ( 12 | PlatformOpenAI Platform = "openai" 13 | PlatformOllama Platform = "ollama" 14 | PlatformAnthropic Platform = "anthropic" 15 | PlatformGemini Platform = "gemini" 16 | ) 17 | 18 | var ( 19 | ErrUnknownPlatform = errors.New("unknown platform") 20 | ) 21 | 22 | type PlatformConfig struct { 23 | ApiKey string 24 | Model string 25 | PromptMaxTokens int64 26 | PromptRequestTimeoutSeconds int64 27 | } 28 | 29 | type ModelRequest struct { 30 | SystemPrompt string 31 | UserPrompt string 32 | } 33 | 34 | type ModelResponse struct { 35 | Content string 36 | } 37 | 38 | type PromptGenerator interface { 39 | GetSystemPrompt() string 40 | GetUserPrompt() string 41 | } 42 | 43 | type PromptExecutor interface { 44 | ExecPrompt(ctx context.Context, promptSource PromptGenerator) (*ModelResponse, error) 45 | } 46 | 47 | func NewPromptExecutor(platform Platform, platformConfig PlatformConfig) (PromptExecutor, error) { 48 | switch platform { 49 | case PlatformOpenAI: 50 | return NewOpenAi(platformConfig), nil 51 | case PlatformOllama: 52 | return NewOllama(platformConfig) 53 | case PlatformAnthropic: 54 | return NewAnthropic(platformConfig), nil 55 | case PlatformGemini: 56 | return NewGemini(platformConfig), nil 57 | default: 58 | return nil, fmt.Errorf("unknown platform %s - %w", platform, ErrUnknownPlatform) 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /pkg/platforms/ollama.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ollama/ollama/api" 8 | ) 9 | 10 | const ( 11 | ollamaDefaultModel = "llama3" 12 | ) 13 | 14 | type Ollama struct { 15 | platformConfig PlatformConfig 16 | 17 | client *api.Client 18 | } 19 | 20 | func NewOllama(platformConfig PlatformConfig) (*Ollama, error) { 21 | client, err := api.ClientFromEnvironment() 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to create Ollama API client: %w", err) 24 | } 25 | 26 | return &Ollama{ 27 | platformConfig: platformConfig, 28 | client: client, 29 | }, nil 30 | } 31 | 32 | func (o *Ollama) ExecPrompt(ctx context.Context, promptSource PromptGenerator) (*ModelResponse, error) { 33 | var targetModel string = ollamaDefaultModel 34 | 35 | // if model is specified by user 36 | if o.platformConfig.Model != "" { 37 | targetModel = o.platformConfig.Model 38 | } 39 | 40 | payload := &api.ChatRequest{ 41 | Model: targetModel, 42 | Messages: []api.Message{ 43 | { 44 | Role: "system", 45 | Content: promptSource.GetSystemPrompt(), 46 | }, 47 | { 48 | Role: "user", 49 | Content: promptSource.GetUserPrompt(), 50 | }, 51 | }, 52 | } 53 | 54 | respFunc := func(resp api.ChatResponse) error { 55 | fmt.Print(resp.Message.Content) 56 | return nil 57 | } 58 | 59 | if err := o.client.Chat(ctx, payload, respFunc); err != nil { 60 | return nil, fmt.Errorf("failed to execute chat request: %w", err) 61 | } 62 | 63 | return &ModelResponse{Content: "response"}, nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/gitgen/config.go: -------------------------------------------------------------------------------- 1 | package gitgen 2 | 3 | type Config struct { 4 | PlatformApiKey string 5 | SourceRef string 6 | DestinationRef string 7 | Platform string 8 | Model string 9 | PromptMaxTokens int64 10 | PromptRequestTimeoutSeconds int64 11 | } 12 | 13 | type ConfigOption func(*Config) 14 | 15 | func WithPlatformApiKey(apiKey string) ConfigOption { 16 | return func(c *Config) { 17 | c.PlatformApiKey = apiKey 18 | } 19 | } 20 | 21 | func WithSourceRef(ref string) ConfigOption { 22 | return func(c *Config) { 23 | c.SourceRef = ref 24 | } 25 | } 26 | 27 | func WithDestinationRef(ref string) ConfigOption { 28 | return func(c *Config) { 29 | c.DestinationRef = ref 30 | } 31 | } 32 | 33 | func WithPlatform(platform string) ConfigOption { 34 | return func(c *Config) { 35 | c.Platform = platform 36 | } 37 | } 38 | 39 | func WithModel(model string) ConfigOption { 40 | return func(c *Config) { 41 | c.Model = model 42 | } 43 | } 44 | 45 | func WithPromptMaxTokens(tokens int64) ConfigOption { 46 | return func(c *Config) { 47 | c.PromptMaxTokens = tokens 48 | } 49 | } 50 | 51 | func WithPromptRequestTimeoutSeconds(timeout int64) ConfigOption { 52 | return func(c *Config) { 53 | c.PromptRequestTimeoutSeconds = timeout 54 | } 55 | } 56 | 57 | func NewConfig(opts ...ConfigOption) *Config { 58 | config := &Config{ 59 | PlatformApiKey: "", 60 | SourceRef: "HEAD", 61 | DestinationRef: "", 62 | Platform: "openai", 63 | Model: "", 64 | PromptMaxTokens: 3500, 65 | PromptRequestTimeoutSeconds: 3600, 66 | } 67 | 68 | for _, opt := range opts { 69 | opt(config) 70 | } 71 | 72 | return config 73 | } 74 | -------------------------------------------------------------------------------- /pkg/gitgen/prompt.go: -------------------------------------------------------------------------------- 1 | package gitgen 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | //go:embed prompts/commit-message.txt 13 | PromptForCommit string 14 | 15 | //go:embed prompts/code-review.txt 16 | PromptForCodeReview string 17 | 18 | //go:embed prompts/test-scenario.txt 19 | PromptForTestScenario string 20 | 21 | //go:embed prompts/test.txt 22 | PromptForTest string 23 | ) 24 | 25 | type PromptAttachment struct { 26 | Filename string 27 | Type string 28 | Content string 29 | } 30 | 31 | type Prompt struct { 32 | ActionType ActionType 33 | 34 | Diff string 35 | Attachments []PromptAttachment 36 | } 37 | 38 | func (p *Prompt) GetSystemPrompt() string { 39 | var instructions string 40 | 41 | instructionsPath := "./instructions.txt" 42 | if _, err := os.Stat(instructionsPath); err == nil { 43 | content, readErr := readFileContent(instructionsPath) 44 | if readErr == nil { 45 | instructions = content 46 | } 47 | } 48 | 49 | var basePrompt string 50 | if p.ActionType == ActionCommitMessage { 51 | basePrompt = PromptForCommit 52 | } else if p.ActionType == ActionCodeReview { 53 | basePrompt = PromptForCodeReview 54 | } else if p.ActionType == ActionTestScenario { 55 | basePrompt = PromptForTestScenario 56 | } else { 57 | basePrompt = PromptForTest 58 | } 59 | 60 | if instructions != "" { 61 | return instructions + "\n\n" + basePrompt 62 | } 63 | 64 | return basePrompt 65 | } 66 | 67 | func (p *Prompt) GetUserPrompt() string { 68 | var builder strings.Builder 69 | 70 | // Add file contents to prompt 71 | for _, attachment := range p.Attachments { 72 | builder.WriteString(fmt.Sprintf("\n\n%s\n```%s\n%s\n```\n\n", 73 | attachment.Filename, 74 | attachment.Type, 75 | attachment.Content)) 76 | } 77 | 78 | builder.WriteString(fmt.Sprintf("\n\n```\n%s\n```\n\n", p.Diff)) 79 | 80 | return builder.String() 81 | } 82 | 83 | func (p *Prompt) Attach(filepath string) error { 84 | content, err := readFileContent(filepath) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | attachment := PromptAttachment{ 90 | Filename: filepath, 91 | Type: getFileExtension(filepath), 92 | Content: content, 93 | } 94 | 95 | p.Attachments = append(p.Attachments, attachment) 96 | 97 | return nil 98 | } 99 | 100 | // readFileContent reads and returns the content of a file 101 | func readFileContent(path string) (string, error) { 102 | content, err := os.ReadFile(path) 103 | 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | return string(content), nil 109 | } 110 | 111 | // getFileExtension returns the file extension without the dot 112 | func getFileExtension(path string) string { 113 | ext := filepath.Ext(path) 114 | 115 | if ext != "" { 116 | return ext[1:] // Remove the leading dot 117 | } 118 | 119 | return "" 120 | } 121 | -------------------------------------------------------------------------------- /pkg/gitgen/register.go: -------------------------------------------------------------------------------- 1 | package gitgen 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | ShellFiles = []string{".zshrc", ".zprofile", ".bashrc", ".bash_profile"} 14 | 15 | ErrShellFileNotFound = errors.New("shell file not found") 16 | ) 17 | 18 | func CheckFileExists(path string) bool { 19 | if _, err := os.Stat(path); err == nil { 20 | return true 21 | } 22 | 23 | return false 24 | } 25 | 26 | func FindShellFile() (string, error) { 27 | userHome, err := os.UserHomeDir() 28 | 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | for _, fileName := range ShellFiles { 34 | filePath := path.Join(userHome, fileName) 35 | 36 | if CheckFileExists(filePath) { 37 | return filePath, nil 38 | } 39 | } 40 | 41 | return "", ErrShellFileNotFound 42 | } 43 | 44 | func UpdatePathLine(existingLine string, newPath string) string { 45 | existingPaths := strings.Split( 46 | strings.Trim(existingLine[12:], `"`), 47 | ":", 48 | ) 49 | 50 | newPaths := []string{} 51 | hasDollarPath := false 52 | for _, path := range existingPaths { 53 | if path == "$PATH" { 54 | hasDollarPath = true 55 | continue 56 | } 57 | 58 | if path == newPath { 59 | continue 60 | } 61 | 62 | newPaths = append(newPaths, path) 63 | } 64 | 65 | if hasDollarPath { 66 | newPaths = append(newPaths, "$PATH") 67 | } 68 | 69 | newPaths = append(newPaths, newPath) 70 | 71 | return fmt.Sprintf(`export PATH="%s"`, strings.Join(newPaths, ":")) 72 | } 73 | 74 | func CreatePathLine(newPath string) string { 75 | return fmt.Sprintf(`export PATH="$PATH:%s"`, newPath) 76 | } 77 | 78 | func ModifyShellFile(filePath string, newPath string) error { 79 | file, err := os.OpenFile(filePath, os.O_RDWR, 0644) 80 | if err != nil { 81 | return err 82 | } 83 | defer file.Close() 84 | 85 | scanner := bufio.NewScanner(file) 86 | var lines []string 87 | var pathLineIndex int = -1 88 | 89 | for scanner.Scan() { 90 | line := scanner.Text() 91 | if strings.HasPrefix(line, "export PATH=") { 92 | pathLineIndex = len(lines) 93 | lines = append(lines, line) 94 | } else { 95 | lines = append(lines, line) 96 | } 97 | } 98 | 99 | if err := scanner.Err(); err != nil { 100 | return err 101 | } 102 | 103 | if pathLineIndex != -1 { 104 | lines[pathLineIndex] = UpdatePathLine(lines[pathLineIndex], newPath) 105 | } else { 106 | lines = append(lines, CreatePathLine(newPath)) 107 | } 108 | 109 | output := strings.Join(lines, "\n") 110 | 111 | if err := os.WriteFile(filePath, []byte(output), 0644); err != nil { 112 | return err 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func RegisterToPath() error { 119 | workingDirectory, err := os.Getwd() 120 | if err != nil { 121 | return err 122 | } 123 | 124 | shellFile, err := FindShellFile() 125 | if err != nil { 126 | return err 127 | } 128 | 129 | err = ModifyShellFile(shellFile, workingDirectory) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/platforms/openai.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | const ( 13 | apiEndpoint = "https://api.openai.com/v1/chat/completions" 14 | openaiDefaultModel = "gpt-4o" 15 | ) 16 | 17 | var ( 18 | ErrPlatformApiKeyIsRequired = errors.New("OpenAI platform requires PLATFORM_API_KEY is specified") 19 | ) 20 | 21 | type openAiPromptRequestMessage struct { 22 | Role string `json:"role"` 23 | Content string `json:"content"` 24 | } 25 | 26 | type openAiPromptRequest struct { 27 | Model string `json:"model"` 28 | Messages []openAiPromptRequestMessage `json:"messages"` 29 | MaxTokens int64 `json:"max_tokens"` 30 | } 31 | 32 | type openAiPromptResponse struct { 33 | Id string `json:"id"` 34 | Object string `json:"object"` 35 | Created int `json:"created"` 36 | Model string `json:"model"` 37 | Usage struct { 38 | PromptTokens int `json:"prompt_tokens"` 39 | CompletionTokens int `json:"completion_tokens"` 40 | TotalTokens int `json:"total_tokens"` 41 | } `json:"usage"` 42 | Choices []struct { 43 | Index int `json:"index"` 44 | Message struct { 45 | Role string `json:"role"` 46 | Content string `json:"content"` 47 | } `json:"message"` 48 | Logprobs *bool `json:"logprobs"` 49 | FinishReason string `json:"finish_reason"` 50 | } `json:"choices"` 51 | } 52 | 53 | type OpenAi struct { 54 | platformConfig PlatformConfig 55 | } 56 | 57 | func NewOpenAi(platformConfig PlatformConfig) *OpenAi { 58 | return &OpenAi{ 59 | platformConfig: platformConfig, 60 | } 61 | } 62 | 63 | func (o *OpenAi) ExecPrompt(ctx context.Context, promptSource PromptGenerator) (*ModelResponse, error) { 64 | if o.platformConfig.ApiKey == "" { 65 | return nil, ErrPlatformApiKeyIsRequired 66 | } 67 | 68 | var targetModel string = openaiDefaultModel 69 | 70 | // if model is specified by user 71 | if o.platformConfig.Model != "" { 72 | targetModel = o.platformConfig.Model 73 | } 74 | 75 | // Create the request body 76 | payload := openAiPromptRequest{ 77 | Model: targetModel, 78 | Messages: []openAiPromptRequestMessage{ 79 | { 80 | Role: "system", 81 | Content: promptSource.GetSystemPrompt(), 82 | }, 83 | { 84 | Role: "user", 85 | Content: promptSource.GetUserPrompt(), 86 | }, 87 | }, 88 | MaxTokens: o.platformConfig.PromptMaxTokens, 89 | } 90 | 91 | body, err := json.MarshalIndent(payload, "", " ") // Use json.MarshalIndent for pretty printing 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // Create the HTTP request 97 | req, err := http.NewRequestWithContext(ctx, "POST", apiEndpoint, bytes.NewBuffer(body)) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | req.Header.Set("Content-Type", "application/json") 103 | req.Header.Set("Authorization", "Bearer "+o.platformConfig.ApiKey) 104 | 105 | // Send the request 106 | client := &http.Client{ 107 | Timeout: time.Duration(o.platformConfig.PromptRequestTimeoutSeconds) * time.Second, 108 | } 109 | res, err := client.Do(req) 110 | if err != nil { 111 | return nil, err 112 | } 113 | defer res.Body.Close() 114 | 115 | var data openAiPromptResponse 116 | 117 | err = json.NewDecoder(res.Body).Decode(&data) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | response := ModelResponse{ 123 | Content: data.Choices[0].Message.Content, 124 | } 125 | 126 | return &response, nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/gitgen/mod.go: -------------------------------------------------------------------------------- 1 | package gitgen 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/go-git/go-git/v5" 10 | "github.com/go-git/go-git/v5/plumbing" 11 | "github.com/seymahandekli/git-gen/pkg/platforms" 12 | ) 13 | 14 | //go:generate stringer -type=ActionType 15 | type ActionType int 16 | 17 | const ( 18 | ActionCommitMessage ActionType = iota 19 | ActionCodeReview 20 | ActionTestScenario 21 | ActionTest 22 | ) 23 | 24 | func runDiffOnCli(config Config) (string, error) { 25 | // Define the Git command 26 | cmdArgs := []string{ 27 | "diff", 28 | "--patch", 29 | "--minimal", 30 | "--diff-algorithm=minimal", 31 | "--ignore-all-space", 32 | "--ignore-blank-lines", 33 | "--no-ext-diff", 34 | "--no-color", 35 | "--unified=10", 36 | config.SourceRef, 37 | } 38 | if config.DestinationRef != "" { 39 | cmdArgs = append(cmdArgs, config.DestinationRef) 40 | } 41 | 42 | cmd := exec.Command("git", cmdArgs...) 43 | 44 | // cmd.Env = os.Environ() 45 | 46 | // var newEnv []string 47 | // for _, e := range cmd.Env { 48 | // if e[:18] != "GIT_EXTERNAL_DIFF=" { 49 | // newEnv = append(newEnv, e) 50 | // } 51 | // } 52 | // cmd.Env = newEnv 53 | 54 | output, err := cmd.Output() 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | // Convert the output to a string 60 | return string(output), nil 61 | } 62 | 63 | func runDiffWithGoGit(config Config) (string, error) { 64 | workingDir, err := os.Getwd() 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | repo, err := git.PlainOpenWithOptions(workingDir, &git.PlainOpenOptions{DetectDotGit: true}) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | srcRefName := plumbing.ReferenceName(config.SourceRef) 75 | if err := srcRefName.Validate(); err != nil { 76 | return "", err 77 | } 78 | srcRef, err := repo.Reference(srcRefName, true) 79 | if err != nil { 80 | return "", err 81 | } 82 | srcCommit, err := repo.CommitObject(srcRef.Hash()) 83 | if err != nil { 84 | return "", err 85 | } 86 | srcTree, err := srcCommit.Tree() 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | var destRef *plumbing.Reference 92 | 93 | if config.DestinationRef != "" { 94 | destRefName := plumbing.ReferenceName(config.DestinationRef) 95 | if err := destRefName.Validate(); err != nil { 96 | return "", err 97 | } 98 | destRef, err = repo.Reference(destRefName, true) 99 | if err != nil { 100 | return "", err 101 | } 102 | } else { 103 | destRef, err = repo.Storer.Reference(plumbing.HEAD) 104 | if err != nil { 105 | return "", err 106 | } 107 | } 108 | 109 | destCommit, err := repo.CommitObject(destRef.Hash()) 110 | if err != nil { 111 | return "", err 112 | } 113 | destTree, err := destCommit.Tree() 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | patch, err := destTree.Diff(srcTree) 119 | if err != nil { 120 | return "", err 121 | } 122 | 123 | return patch.String(), nil 124 | } 125 | 126 | func Do(actionType ActionType, config Config) (string, error) { 127 | // Run the git diff command 128 | diff, err := runDiffOnCli(config) 129 | if err != nil { 130 | return "", err 131 | } 132 | 133 | platformConfig := platforms.PlatformConfig{ 134 | ApiKey: config.PlatformApiKey, 135 | Model: config.Model, 136 | PromptMaxTokens: config.PromptMaxTokens, 137 | PromptRequestTimeoutSeconds: config.PromptRequestTimeoutSeconds, 138 | } 139 | 140 | // Convert string to Platform type 141 | platform := platforms.Platform(config.Platform) 142 | runtime, err := platforms.NewPromptExecutor(platform, platformConfig) 143 | if err != nil { 144 | return "", err 145 | } 146 | prompt := &Prompt{ 147 | ActionType: actionType, 148 | Diff: diff, 149 | Attachments: []PromptAttachment{}, 150 | } 151 | 152 | log.Printf("System Prompt:\n%s\n\n", prompt.GetSystemPrompt()) 153 | // log.Printf("User Prompt:\n%s\n\n", prompt.GetUserPrompt()) 154 | log.Printf("User Prompt Length:\n%d\n\n", len(prompt.GetUserPrompt())) 155 | 156 | response, err := runtime.ExecPrompt(context.Background(), prompt) 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | return response.Content, nil 162 | } 163 | -------------------------------------------------------------------------------- /pkg/platforms/gemini.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | geminiApiEndpoint = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions" 18 | geminiDefaultModel = "gemini-1.5-pro" 19 | maxScannerBufferSize = 10 * 1024 * 1024 // 10 MB buffer 20 | ) 21 | 22 | var ( 23 | ErrGeminiApiKeyIsRequired = errors.New("Gemini platform requires PLATFORM_API_KEY to be specified") 24 | ) 25 | 26 | type geminiPromptRequestMessage struct { 27 | Role string `json:"role"` 28 | Content string `json:"content"` 29 | } 30 | 31 | type geminiPromptRequest struct { 32 | Model string `json:"model"` 33 | Messages []geminiPromptRequestMessage `json:"messages"` 34 | Stream bool `json:"stream"` 35 | } 36 | 37 | type geminiPromptResponseChunk struct { 38 | Choices []struct { 39 | Delta struct { 40 | Content string `json:"content"` 41 | } `json:"delta"` 42 | } `json:"choices"` 43 | } 44 | 45 | type Gemini struct { 46 | platformConfig PlatformConfig 47 | } 48 | 49 | func NewGemini(platformConfig PlatformConfig) *Gemini { 50 | return &Gemini{ 51 | platformConfig: platformConfig, 52 | } 53 | } 54 | 55 | func (g *Gemini) ExecPrompt(ctx context.Context, promptSource PromptGenerator) (*ModelResponse, error) { 56 | if strings.TrimSpace(g.platformConfig.ApiKey) == "" { 57 | return nil, ErrGeminiApiKeyIsRequired 58 | } 59 | 60 | model := geminiDefaultModel 61 | if g.platformConfig.Model != "" { 62 | model = g.platformConfig.Model 63 | } 64 | 65 | payload := geminiPromptRequest{ 66 | Model: model, 67 | Messages: []geminiPromptRequestMessage{ 68 | {Role: "system", Content: promptSource.GetSystemPrompt()}, 69 | {Role: "user", Content: promptSource.GetUserPrompt()}, 70 | }, 71 | Stream: true, 72 | } 73 | 74 | body, err := json.Marshal(payload) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | req, err := http.NewRequestWithContext(ctx, "POST", geminiApiEndpoint, bytes.NewBuffer(body)) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | req.Header.Set("Content-Type", "application/json") 85 | req.Header.Set("Authorization", "Bearer "+g.platformConfig.ApiKey) 86 | 87 | client := &http.Client{ 88 | Timeout: 60 * time.Second, 89 | } 90 | res, err := client.Do(req) 91 | if err != nil { 92 | return nil, err 93 | } 94 | defer res.Body.Close() 95 | 96 | if res.StatusCode == http.StatusTooManyRequests { 97 | retryAfter := res.Header.Get("Retry-After") 98 | return nil, fmt.Errorf("Rate limit exceeded. Retry after %s seconds", retryAfter) 99 | } 100 | 101 | if res.StatusCode != http.StatusOK { 102 | body, _ := io.ReadAll(res.Body) 103 | return nil, fmt.Errorf("Gemini API request failed with status code %d. Response: %s", res.StatusCode, string(body)) 104 | } 105 | 106 | scanner := bufio.NewScanner(res.Body) 107 | buf := make([]byte, maxScannerBufferSize) 108 | scanner.Buffer(buf, maxScannerBufferSize) 109 | 110 | var responseBuilder strings.Builder 111 | for scanner.Scan() { 112 | line := scanner.Text() 113 | 114 | // Skip empty lines or lines that start with non-data prefixes 115 | if len(line) == 0 || !strings.HasPrefix(line, "data: ") { 116 | continue 117 | } 118 | 119 | // Remove the "data: " prefix 120 | line = strings.TrimPrefix(line, "data: ") 121 | 122 | // Check for the "[DONE]" marker 123 | if line == "[DONE]" { 124 | break // End of stream 125 | } 126 | 127 | // Parse the JSON chunk 128 | var chunk geminiPromptResponseChunk 129 | if err := json.Unmarshal([]byte(line), &chunk); err != nil { 130 | return nil, fmt.Errorf("Failed to parse chunk: %v. Line: %s", err, line) 131 | } 132 | 133 | // Append the content to the response builder 134 | if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" { 135 | responseBuilder.WriteString(chunk.Choices[0].Delta.Content) 136 | } 137 | } 138 | 139 | if err := scanner.Err(); err != nil { 140 | return nil, err 141 | } 142 | 143 | // Return the final model response 144 | return &ModelResponse{ 145 | Content: responseBuilder.String(), 146 | }, nil 147 | 148 | } 149 | -------------------------------------------------------------------------------- /pkg/platforms/anthropic.go: -------------------------------------------------------------------------------- 1 | package platforms 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | const ( 14 | anthropicApiEndpoint = "https://api.anthropic.com/v1/messages" 15 | // Using Claude 3.5 Sonnet as default model based on docs 16 | anthropicDefaultModel = "claude-3-5-sonnet-20241022" 17 | anthropicApiVersion = "2023-06-01" 18 | ) 19 | 20 | var ( 21 | ErrAnthropicApiKeyIsRequired = errors.New("Anthropic platform requires PLATFORM_API_KEY is specified") 22 | ) 23 | 24 | // Request structures 25 | type anthropicMessage struct { 26 | Role string `json:"role"` 27 | Content string `json:"content"` 28 | } 29 | 30 | type anthropicRequest struct { 31 | Model string `json:"model"` 32 | Messages []anthropicMessage `json:"messages"` 33 | MaxTokens int64 `json:"max_tokens"` 34 | System string `json:"system,omitempty"` 35 | } 36 | 37 | // Response structures 38 | type anthropicContentBlock struct { 39 | Type string `json:"type"` 40 | Text string `json:"text"` 41 | } 42 | 43 | type anthropicResponse struct { 44 | Id string `json:"id"` 45 | Type string `json:"type"` 46 | Role string `json:"role"` 47 | Content []anthropicContentBlock `json:"content"` 48 | Model string `json:"model"` 49 | Usage struct { 50 | InputTokens int `json:"input_tokens"` 51 | OutputTokens int `json:"output_tokens"` 52 | } `json:"usage"` 53 | StopReason string `json:"stop_reason"` 54 | StopSequence *string `json:"stop_sequence"` 55 | } 56 | 57 | type anthropicErrorDetail struct { 58 | Type string `json:"type"` 59 | Message string `json:"message"` 60 | } 61 | 62 | type anthropicErrorResponse struct { 63 | Type string `json:"type"` 64 | Error anthropicErrorDetail `json:"error"` 65 | } 66 | 67 | type Anthropic struct { 68 | platformConfig PlatformConfig 69 | } 70 | 71 | func NewAnthropic(platformConfig PlatformConfig) *Anthropic { 72 | return &Anthropic{ 73 | platformConfig: platformConfig, 74 | } 75 | } 76 | 77 | func (a *Anthropic) ExecPrompt(ctx context.Context, promptSource PromptGenerator) (*ModelResponse, error) { 78 | if a.platformConfig.ApiKey == "" { 79 | return nil, ErrAnthropicApiKeyIsRequired 80 | } 81 | 82 | targetModel := anthropicDefaultModel 83 | if a.platformConfig.Model != "" { 84 | targetModel = a.platformConfig.Model 85 | } 86 | 87 | // Create request payload 88 | payload := anthropicRequest{ 89 | Model: targetModel, 90 | Messages: []anthropicMessage{ 91 | { 92 | Role: "user", 93 | Content: promptSource.GetUserPrompt(), 94 | }, 95 | }, 96 | MaxTokens: a.platformConfig.PromptMaxTokens, 97 | } 98 | 99 | // Add system prompt if provided 100 | if systemPrompt := promptSource.GetSystemPrompt(); systemPrompt != "" { 101 | payload.System = systemPrompt 102 | } 103 | 104 | body, err := json.Marshal(payload) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | // Create HTTP request 110 | req, err := http.NewRequestWithContext(ctx, "POST", anthropicApiEndpoint, bytes.NewBuffer(body)) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | // Set required headers 116 | req.Header.Set("Content-Type", "application/json") 117 | req.Header.Set("x-api-key", a.platformConfig.ApiKey) 118 | req.Header.Set("anthropic-version", anthropicApiVersion) 119 | 120 | // Send request 121 | client := &http.Client{ 122 | Timeout: time.Duration(a.platformConfig.PromptRequestTimeoutSeconds) * time.Second, 123 | } 124 | res, err := client.Do(req) 125 | if err != nil { 126 | return nil, err 127 | } 128 | defer res.Body.Close() 129 | 130 | if res.StatusCode != http.StatusOK { 131 | var errResp anthropicErrorResponse 132 | if err := json.NewDecoder(res.Body).Decode(&errResp); err != nil { 133 | return nil, fmt.Errorf("anthropic API error (status %d): failed to decode error response: %w", 134 | res.StatusCode, err) 135 | } 136 | return nil, fmt.Errorf("anthropic API error: %s - %s", 137 | errResp.Error.Type, errResp.Error.Message) 138 | } 139 | 140 | var data anthropicResponse 141 | err = json.NewDecoder(res.Body).Decode(&data) 142 | if err != nil { 143 | return nil, fmt.Errorf("failed to decode successful response: %w", err) 144 | } 145 | 146 | // Check for error type in successful response 147 | if data.Type == "error" { 148 | return nil, fmt.Errorf("anthropic API returned error type in response") 149 | } 150 | 151 | // Extract text content from the first content block 152 | var content string 153 | if len(data.Content) > 0 { 154 | content = data.Content[0].Text 155 | } 156 | 157 | response := ModelResponse{ 158 | Content: content, 159 | } 160 | 161 | return &response, nil 162 | } 163 | -------------------------------------------------------------------------------- /cmd/git-gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | 8 | "github.com/seymahandekli/git-gen/pkg/gitgen" 9 | "github.com/urfave/cli/v3" 10 | ) 11 | 12 | func main() { 13 | var platformApiKey string 14 | var sourceRef string 15 | var destinationRef string 16 | var platform string 17 | var model string 18 | var maxTokens int64 19 | 20 | log.SetFlags(0) 21 | 22 | cmd := &cli.Command{ 23 | Name: "git-gen", 24 | Usage: "Generate commit messages and perform code reviews using ChatGPT", 25 | 26 | Commands: []*cli.Command{ 27 | { 28 | Name: "commit", 29 | Usage: "Generates a commit message", 30 | Flags: []cli.Flag{ 31 | &cli.StringFlag{ 32 | Name: "apikey", 33 | Usage: "Platform API key", 34 | Sources: cli.EnvVars("PLATFORM_API_KEY"), 35 | Destination: &platformApiKey, 36 | }, 37 | &cli.StringFlag{ 38 | Name: "source", 39 | Usage: "Source Ref", 40 | Value: "HEAD", 41 | Destination: &sourceRef, 42 | }, 43 | &cli.StringFlag{ 44 | Name: "dest", 45 | Usage: "Destination Ref", 46 | Value: "", 47 | Destination: &destinationRef, 48 | }, 49 | &cli.StringFlag{ 50 | Name: "platform", 51 | Usage: "Platform", 52 | Value: "openai", 53 | Destination: &platform, 54 | }, 55 | &cli.StringFlag{ 56 | Name: "model", 57 | Usage: "Model", 58 | Value: "", 59 | Destination: &model, 60 | }, 61 | &cli.IntFlag{ 62 | Name: "maxtokens", 63 | Usage: "Maximum tokens to generate", 64 | Value: 3500, 65 | Destination: &maxTokens, 66 | }, 67 | }, 68 | Action: func(ctx context.Context, cmd *cli.Command) error { 69 | config := gitgen.NewConfig( 70 | gitgen.WithPlatformApiKey(platformApiKey), 71 | gitgen.WithSourceRef(sourceRef), 72 | gitgen.WithDestinationRef(destinationRef), 73 | gitgen.WithPlatform(platform), 74 | gitgen.WithModel(model), 75 | gitgen.WithPromptMaxTokens(maxTokens), 76 | ) 77 | 78 | result, err := gitgen.Do(gitgen.ActionCommitMessage, *config) 79 | 80 | if err != nil { 81 | return err 82 | } 83 | 84 | log.Println(result) 85 | return nil 86 | }, 87 | }, 88 | { 89 | Name: "review", 90 | Usage: "Performs a code review", 91 | Flags: []cli.Flag{ 92 | &cli.StringFlag{ 93 | Name: "apikey", 94 | Usage: "Platform API key", 95 | Sources: cli.EnvVars("PLATFORM_API_KEY"), 96 | Destination: &platformApiKey, 97 | }, 98 | &cli.StringFlag{ 99 | Name: "source", 100 | Usage: "Source Ref", 101 | Value: "HEAD", 102 | Destination: &sourceRef, 103 | }, 104 | &cli.StringFlag{ 105 | Name: "dest", 106 | Usage: "Destination Ref", 107 | Value: "", 108 | Destination: &destinationRef, 109 | }, 110 | &cli.StringFlag{ 111 | Name: "platform", 112 | Usage: "Platform", 113 | Value: "openai", 114 | Destination: &platform, 115 | }, 116 | &cli.StringFlag{ 117 | Name: "model", 118 | Usage: "Model", 119 | Value: "", 120 | Destination: &model, 121 | }, 122 | &cli.IntFlag{ 123 | Name: "maxtokens", 124 | Usage: "Maximum tokens to generate", 125 | Value: 3500, 126 | Destination: &maxTokens, 127 | }, 128 | }, 129 | Action: func(ctx context.Context, cmd *cli.Command) error { 130 | config := gitgen.NewConfig( 131 | gitgen.WithPlatformApiKey(platformApiKey), 132 | gitgen.WithSourceRef(sourceRef), 133 | gitgen.WithDestinationRef(destinationRef), 134 | gitgen.WithPlatform(platform), 135 | gitgen.WithModel(model), 136 | gitgen.WithPromptMaxTokens(maxTokens), 137 | ) 138 | 139 | result, err := gitgen.Do(gitgen.ActionCodeReview, *config) 140 | 141 | if err != nil { 142 | return err 143 | } 144 | 145 | log.Println(result) 146 | return nil 147 | }, 148 | }, 149 | { 150 | Name: "test-scenarios", 151 | Usage: "Creating test scenarios", 152 | Flags: []cli.Flag{ 153 | &cli.StringFlag{ 154 | Name: "apikey", 155 | Usage: "Platform API key", 156 | Sources: cli.EnvVars("PLATFORM_API_KEY"), 157 | Destination: &platformApiKey, 158 | }, 159 | &cli.StringFlag{ 160 | Name: "source", 161 | Usage: "Source Ref", 162 | Value: "HEAD", 163 | Destination: &sourceRef, 164 | }, 165 | &cli.StringFlag{ 166 | Name: "dest", 167 | Usage: "Destination Ref", 168 | Value: "", 169 | Destination: &destinationRef, 170 | }, 171 | &cli.StringFlag{ 172 | Name: "platform", 173 | Usage: "Platform", 174 | Value: "openai", 175 | Destination: &platform, 176 | }, 177 | &cli.StringFlag{ 178 | Name: "model", 179 | Usage: "Model", 180 | Value: "", 181 | Destination: &model, 182 | }, 183 | &cli.IntFlag{ 184 | Name: "maxtokens", 185 | Usage: "Maximum tokens to generate", 186 | Value: 3500, 187 | Destination: &maxTokens, 188 | }, 189 | }, 190 | Action: func(ctx context.Context, cmd *cli.Command) error { 191 | config := gitgen.NewConfig( 192 | gitgen.WithPlatformApiKey(platformApiKey), 193 | gitgen.WithSourceRef(sourceRef), 194 | gitgen.WithDestinationRef(destinationRef), 195 | gitgen.WithPlatform(platform), 196 | gitgen.WithModel(model), 197 | gitgen.WithPromptMaxTokens(maxTokens), 198 | ) 199 | 200 | result, err := gitgen.Do(gitgen.ActionTestScenario, *config) 201 | 202 | if err != nil { 203 | return err 204 | } 205 | 206 | log.Println(result) 207 | return nil 208 | }, 209 | }, 210 | { 211 | Name: "test", 212 | Usage: "Creating tests", 213 | Flags: []cli.Flag{ 214 | &cli.StringFlag{ 215 | Name: "apikey", 216 | Usage: "Platform API key", 217 | Sources: cli.EnvVars("PLATFORM_API_KEY"), 218 | Destination: &platformApiKey, 219 | }, 220 | &cli.StringFlag{ 221 | Name: "source", 222 | Usage: "Source Ref", 223 | Value: "HEAD", 224 | Destination: &sourceRef, 225 | }, 226 | &cli.StringFlag{ 227 | Name: "dest", 228 | Usage: "Destination Ref", 229 | Value: "", 230 | Destination: &destinationRef, 231 | }, 232 | &cli.StringFlag{ 233 | Name: "platform", 234 | Usage: "Platform", 235 | Value: "openai", 236 | Destination: &platform, 237 | }, 238 | &cli.StringFlag{ 239 | Name: "model", 240 | Usage: "Model", 241 | Value: "", 242 | Destination: &model, 243 | }, 244 | &cli.IntFlag{ 245 | Name: "maxtokens", 246 | Usage: "Maximum tokens to generate", 247 | Value: 3500, 248 | Destination: &maxTokens, 249 | }, 250 | }, 251 | Action: func(ctx context.Context, cmd *cli.Command) error { 252 | config := gitgen.NewConfig( 253 | gitgen.WithPlatformApiKey(platformApiKey), 254 | gitgen.WithSourceRef(sourceRef), 255 | gitgen.WithDestinationRef(destinationRef), 256 | gitgen.WithPlatform(platform), 257 | gitgen.WithModel(model), 258 | gitgen.WithPromptMaxTokens(maxTokens), 259 | ) 260 | 261 | result, err := gitgen.Do(gitgen.ActionTest, *config) 262 | 263 | if err != nil { 264 | return err 265 | } 266 | 267 | log.Println(result) 268 | return nil 269 | }, 270 | }, 271 | { 272 | Name: "register", 273 | Usage: "Registers itself to the running system", 274 | Action: func(ctx context.Context, cmd *cli.Command) error { 275 | err := gitgen.RegisterToPath() 276 | 277 | return err 278 | }, 279 | }, 280 | }, 281 | } 282 | 283 | if err := cmd.Run(context.Background(), os.Args); err != nil { 284 | log.Fatal(err) 285 | return 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-gen 2 | 3 | `git-gen` is a command-line tool developed in Go that generates commit messages and code reviews based on code changes in your project by utilizing OpenAI's ChatGPT API, Ollama API, Anthropic's Claude API, and Google's Gemini API. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Features](#features) 9 | - [Installation](#installation) 10 | - [Commands](#commands) 11 | - [Supported Platforms](#supported-platforms) 12 | - [Configuration](#configuration) 13 | - [Custom Instructions](#custom-instructions) 14 | - [Contributing](#contributing) 15 | - [License](#license) 16 | 17 | ## Introduction 18 | 19 | `git-gen` is designed to assist developers in creating detailed commit messages and/or performing code reviews automatically depending on their codebase changes. By leveraging the power of ChatGPT, Ollama, Gemini, and Claude, `git-gen` analyzes the changes made to the code and generates meaningful output. 20 | 21 | ## Features 22 | 23 | - Generate commit messages based on code changes 24 | - Perform detailed code reviews 25 | - Support for multiple AI platforms (OpenAI, Ollama, Anthropic, Gemini) 26 | - Customizable model selection 27 | - Configurable token limits and timeout settings 28 | - Test scenario and test code generation 29 | - Custom instruction support via instructions.txt 30 | - Native Git diff implementation for accurate change detection 31 | 32 | ## Installation 33 | 34 | To get started with `git-gen`, you need to have Go installed on your machine. You can download and install Go from [here](https://golang.org/dl/). 35 | 36 | Once Go is installed, you can clone the `git-gen` repository and build the tool: 37 | 38 | ```sh 39 | git clone https://github.com/seymahandekli/git-gen 40 | cd git-gen 41 | 42 | go build ./cmd/git-gen 43 | ./git-gen register 44 | ``` 45 | 46 | You can install the package by putting the /usr/local/go/bin directory in your PATH environment variable: 47 | 48 | ```sh 49 | go install github.com/seymahandekli/git-gen/cmd/git-gen@latest 50 | ``` 51 | 52 | ## Commands 53 | 54 | ### commit 55 | Generates a commit message based on your code changes. 56 | 57 | ```sh 58 | # Basic usage 59 | git gen commit 60 | 61 | # With specific source and destination 62 | git gen commit --source "commitID" --dest "commitID" 63 | 64 | # With platform and model 65 | git gen commit --platform openai --model gpt-4 66 | ``` 67 | 68 | ### review 69 | Performs a code review of your changes. 70 | 71 | ```sh 72 | # Basic usage 73 | git gen review 74 | 75 | # With specific source and destination 76 | git gen review --source "commitID" --dest "commitID" 77 | 78 | # With platform and model 79 | git gen review --platform anthropic --model claude-3-sonnet 80 | ``` 81 | 82 | ### test-scenarios 83 | Creates test scenarios based on your code changes. 84 | 85 | ```sh 86 | # Basic usage 87 | git gen test-scenarios 88 | 89 | # With specific source and destination 90 | git gen test-scenarios --source "commitID" --dest "commitID" 91 | 92 | # With platform and model 93 | git gen test-scenarios --platform gemini --model gemini-pro 94 | ``` 95 | 96 | ### test 97 | Creates test implementations based on your code changes. 98 | 99 | ```sh 100 | # Basic usage 101 | git gen test 102 | 103 | # With specific source and destination 104 | git gen test --source "commitID" --dest "commitID" 105 | 106 | # With platform and model 107 | git gen test --platform ollama --model llama2 108 | ``` 109 | 110 | ### register 111 | Registers the tool to your system for global usage. 112 | 113 | ```sh 114 | git gen register 115 | ``` 116 | 117 | ### help 118 | Shows help information for commands. 119 | 120 | ```sh 121 | # Show all commands 122 | git gen help 123 | 124 | # Show help for specific command 125 | git gen help commit 126 | git gen help review 127 | git gen help test-scenarios 128 | git gen help test 129 | ``` 130 | 131 | ## Supported Platforms 132 | 133 | `git-gen` supports multiple AI platforms, each with its own characteristics and requirements. 134 | 135 | ### OpenAI 136 | OpenAI's models provide high-quality responses and are suitable for most use cases. 137 | 138 | ```sh 139 | # Basic usage with OpenAI 140 | git gen commit --platform openai --model gpt-4 141 | 142 | # Available models: 143 | # - gpt-4 144 | # - gpt-3.5-turbo 145 | # - gpt-4-turbo-preview 146 | ``` 147 | 148 | **Requirements:** 149 | - OpenAI API key (set via `--apikey` or `PLATFORM_API_KEY` environment variable) 150 | - Internet connection 151 | - Paid API access 152 | 153 | ### Ollama 154 | Ollama provides local model deployment, making it perfect for offline use and privacy-focused development. 155 | 156 | ```sh 157 | # Basic usage with Ollama 158 | git gen commit --platform ollama --model llama2 159 | 160 | # Available models: 161 | # - llama2 162 | # - mistral 163 | # - codellama 164 | # - neural-chat 165 | ``` 166 | 167 | **Requirements:** 168 | - Ollama installed locally 169 | - No API key required 170 | - Sufficient local computing resources 171 | 172 | ### Anthropic 173 | Anthropic's Claude models excel at understanding and generating code-related content. 174 | 175 | ```sh 176 | # Basic usage with Anthropic 177 | git gen commit --platform anthropic --model claude-3-sonnet 178 | 179 | # Available models: 180 | # - claude-3-sonnet 181 | # - claude-3-opus 182 | # - claude-3-haiku 183 | ``` 184 | 185 | **Requirements:** 186 | - Anthropic API key (set via `--apikey` or `PLATFORM_API_KEY` environment variable) 187 | - Internet connection 188 | - Paid API access 189 | 190 | ### Gemini 191 | Google's Gemini models provide fast and efficient responses, particularly good for code-related tasks. 192 | 193 | ```sh 194 | # Basic usage with Gemini 195 | git gen commit --platform gemini --model gemini-pro 196 | 197 | # Available models: 198 | # - gemini-pro 199 | # - gemini-pro-vision 200 | ``` 201 | 202 | **Requirements:** 203 | - Google API key (set via `--apikey` or `PLATFORM_API_KEY` environment variable) 204 | - Internet connection 205 | - Paid API access 206 | 207 | ## Configuration 208 | 209 | `git-gen` supports various configuration options that can be set through command-line flags or environment variables: 210 | 211 | ### Platform Options 212 | - `--platform`: Choose between "openai", "ollama", "anthropic", or "gemini" (default: "openai") 213 | - `--model`: Specify the AI model to use (e.g., "gpt-4", "llama2", "claude-3-sonnet", "gemini-pro") 214 | - `--apikey`: Your platform API key (can also be set via PLATFORM_API_KEY environment variable) 215 | 216 | ### Token and Timeout Settings 217 | - `--prompt-max-tokens`: Maximum tokens for the prompt (default: 3500) 218 | - `--prompt-timeout`: Request timeout in seconds (default: 3600) 219 | 220 | ### Reference Settings 221 | - `--source`: Source reference for diff (default: "HEAD") 222 | - `--dest`: Destination reference for diff 223 | 224 | ### Example Configuration 225 | 226 | ```sh 227 | # Using OpenAI with custom token limit 228 | git gen commit --platform openai --model gpt-4 --prompt-max-tokens 4000 229 | 230 | # Using Ollama with custom timeout 231 | git gen commit --platform ollama --model llama2 --prompt-timeout 1800 232 | 233 | # Using Anthropic with custom source/destination 234 | git gen commit --platform anthropic --model claude-3-sonnet --source HEAD~2 --dest HEAD 235 | 236 | # Using Gemini with custom configuration 237 | git gen commit --platform gemini --model gemini-pro --prompt-max-tokens 4000 238 | ``` 239 | 240 | ## Custom Instructions 241 | 242 | You can provide custom instructions for any generation task by creating an `instructions.txt` file in your project root. This file will be automatically picked up and used to customize the generation process. 243 | 244 | ### Using git-gen-dotfile-generator 245 | 246 | The easiest way to create your `instructions.txt` file is to use the [git-gen-dotfile-generator](https://github.com/seymahandekli/git-gen-dotfile-generator) tool. This web-based tool provides a user-friendly interface to generate AI instructions and rules for your project. 247 | 248 | #### Features of git-gen-dotfile-generator: 249 | - Specify project name, role, and expertise areas 250 | - Define coding preferences and standards 251 | - Add sample code to create contextual rules 252 | - Download the generated instructions/rules directly 253 | 254 | #### How to use: 255 | 1. Visit [git-gen-dotfile-generator](https://github.com/seymahandekli/git-gen-dotfile-generator) 256 | 2. Fill out the form with your project details: 257 | - Project name 258 | - Role 259 | - Expertise areas 260 | - Coding preferences 261 | 3. Add any sample code if needed 262 | 4. Generate and download your `instructions.txt` file 263 | 5. Place the file in your project root directory 264 | 265 | The `instructions.txt` file will be automatically used by `git-gen` to: 266 | - Generate more relevant commit messages 267 | - Create more accurate code reviews 268 | - Generate appropriate test scenarios 269 | - Maintain consistency with your project standards 270 | 271 | ## Contributing 272 | 273 | We welcome contributions from the community! If you'd like to contribute to `git-gen`, please follow these steps: 274 | 275 | 1. Fork the repository. 276 | 2. Create a new branch for your feature or bugfix. 277 | 3. Make your changes and commit them with clear messages. 278 | 4. Push your changes to your fork. 279 | 5. Submit a pull request to the `main` branch of this repository. 280 | 281 | For major changes, please open an issue first to discuss what you would like to change. 282 | 283 | ## License 284 | 285 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 286 | 287 | ## Contributors 288 | 289 | - Eser Özvataf (https://github.com/eser) 290 | - Daniel M. Matongo (https://github.com/mmatongo) 291 | 292 | ## Acknowledgement 293 | 294 | I would like to thank people below for their support and contributions: 295 | 296 | - Arda Kılıçdağı (http://github.com/Ardakilic) 297 | - Erman İmer (https://github.com/ermanimer) 298 | - Eser Özvataf (https://github.com/eser) 299 | 300 | --- 301 | 302 | We hope you find `git-gen` useful! If you have any questions or feedback, please feel free to open an issue on GitHub. 303 | 304 | Happy coding! 305 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 5 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 6 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 7 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 8 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 13 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 14 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 15 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 16 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 17 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 22 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 23 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 24 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 25 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 26 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 27 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 28 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 29 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 30 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 31 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 32 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 33 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 34 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 35 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 36 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 37 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 38 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 39 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 40 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 41 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 42 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 43 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 44 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 45 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/ollama/ollama v0.3.1 h1:FvbhD9TxSB1F2xvQPFaGvYKLVxK9QJqfU+EUb3ftwkE= 51 | github.com/ollama/ollama v0.3.1/go.mod h1:USAVO5xFaXAoVWJ0rkPYgCVhTxE/oJ81o7YGcJxvyp8= 52 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 53 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 54 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 55 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 56 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 57 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 61 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 62 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 63 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 64 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 65 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 66 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 69 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 70 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 71 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 72 | github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= 73 | github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc= 74 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 75 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 76 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 77 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 78 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 80 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 81 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 82 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 83 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 84 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 85 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 86 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 87 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 88 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 89 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 90 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 92 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 93 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 94 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 95 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 96 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 97 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 98 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 99 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 100 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 101 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 102 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 103 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 104 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 105 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 118 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 119 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 120 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 121 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 122 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 123 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 124 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 125 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 126 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 127 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 128 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 129 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 130 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 131 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 132 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 133 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 134 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 135 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 136 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 137 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 138 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 139 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 140 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 141 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 143 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 144 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 145 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 146 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 147 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 148 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 149 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 150 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 151 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 152 | --------------------------------------------------------------------------------