├── .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 |
--------------------------------------------------------------------------------