Skip to content

Commit fa3fcbc

Browse files
committed
-a5ed7c2: Initial implementation of Ollama providdser integration
0 parents  commit fa3fcbc

12 files changed

Lines changed: 439 additions & 0 deletions

File tree

cmd/config.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// File: cmd/config.go
2+
package cmd
3+
4+
import (
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"smartcommit/config"
12+
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var ConfigCmd = &cobra.Command{
17+
Use: "config",
18+
Short: "View or change smartcommit configuration",
19+
}
20+
21+
var setCmd = &cobra.Command{
22+
Use: "set <key> <value>",
23+
Short: "Set a configuration value",
24+
Args: cobra.ExactArgs(2),
25+
Run: func(cmd *cobra.Command, args []string) {
26+
key, value := args[0], args[1]
27+
cfg := config.LoadOrDefault()
28+
cfg.Set(key, value)
29+
if err := config.Save(cfg); err != nil {
30+
fmt.Println("❌ Failed to save config:", err)
31+
return
32+
}
33+
fmt.Println("✅ Config updated")
34+
},
35+
}
36+
37+
var showCmd = &cobra.Command{
38+
Use: "show",
39+
Short: "Display current configuration",
40+
Run: func(cmd *cobra.Command, args []string) {
41+
cfg := config.LoadOrDefault()
42+
cfg.PrettyPrint()
43+
},
44+
}
45+
46+
var editCmd = &cobra.Command{
47+
Use: "edit system_prompt",
48+
Short: "Edit the system prompt using your default editor",
49+
Args: cobra.ExactArgs(1),
50+
Run: func(cmd *cobra.Command, args []string) {
51+
if args[0] != "system_prompt" {
52+
fmt.Println("❌ Only 'system_prompt' can be edited via editor for now.")
53+
return
54+
}
55+
56+
cfg := config.LoadOrDefault()
57+
tmpfile := filepath.Join(os.TempDir(), "smartcommit_system_prompt.txt")
58+
os.WriteFile(tmpfile, []byte(cfg.SystemPrompt), 0644)
59+
60+
editor := os.Getenv("EDITOR")
61+
if editor == "" {
62+
editor = "vim"
63+
}
64+
65+
execCmd := exec.Command(editor, tmpfile)
66+
execCmd.Stdin = os.Stdin
67+
execCmd.Stdout = os.Stdout
68+
execCmd.Stderr = os.Stderr
69+
execCmd.Run()
70+
71+
updated, _ := os.ReadFile(tmpfile)
72+
cfg.SystemPrompt = strings.TrimSpace(string(updated))
73+
if err := config.Save(cfg); err != nil {
74+
fmt.Println("❌ Failed to save:", err)
75+
return
76+
}
77+
fmt.Println("✅ Updated system prompt")
78+
},
79+
}
80+
81+
func init() {
82+
ConfigCmd.AddCommand(setCmd, showCmd, editCmd)
83+
}

cmd/generate.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"smartcommit/config"
9+
"smartcommit/diff"
10+
"smartcommit/llm"
11+
"smartcommit/prompt"
12+
13+
promptui "github.com/manifoldco/promptui"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
var GenerateCmd = &cobra.Command{
18+
Use: "generate",
19+
Short: "Generate a commit message using AI",
20+
Run: func(cmd *cobra.Command, args []string) {
21+
cfg := config.LoadOrDefault()
22+
23+
24+
diffText, err := diff.GetStagedDiff()
25+
if err != nil || len(diffText) == 0 {
26+
fmt.Println("❌ No staged changes found.")
27+
return
28+
}
29+
30+
promptText := prompt.Build(diffText, cfg.SystemPrompt)
31+
32+
provider, err := llm.GetProvider(cfg)
33+
if err != nil {
34+
fmt.Println("❌", err)
35+
return
36+
}
37+
38+
message, err := provider.Generate(promptText)
39+
if err != nil {
40+
fmt.Println("❌ Generation failed:", err)
41+
return
42+
}
43+
44+
reader := bufio.NewReader(os.Stdin)
45+
46+
for {
47+
fmt.Println("\n💡 Generated Commit Message:")
48+
fmt.Println("----------------------------------")
49+
fmt.Println(message)
50+
fmt.Println("----------------------------------")
51+
fmt.Print("Choose: [c]ommit, [e]dit prompt, [r]egenerate, [q]uit: ")
52+
53+
choice, _ := reader.ReadString('\n')
54+
switch choice[:1] {
55+
case "c":
56+
runGitCommit(message)
57+
return
58+
case "e":
59+
promptText = editPrompt(promptText)
60+
message, _ = provider.Generate(promptText)
61+
case "r":
62+
message, _ = provider.Generate(promptText)
63+
case "q":
64+
return
65+
default:
66+
fmt.Println("❓ Invalid choice.")
67+
}
68+
}
69+
},
70+
}
71+
72+
func runGitCommit(msg string) {
73+
fmt.Println("✅ Committing with:")
74+
fmt.Println(msg)
75+
_ = os.WriteFile(".git/COMMIT_EDITMSG", []byte(msg), 0644)
76+
_ = executeCommand("git", "commit", "-F", ".git/COMMIT_EDITMSG")
77+
}
78+
79+
func executeCommand(name string, args ...string) error {
80+
c := exec.Command(name, args...)
81+
c.Stdin = os.Stdin
82+
c.Stdout = os.Stdout
83+
c.Stderr = os.Stderr
84+
return c.Run()
85+
}
86+
87+
func editPrompt(defaultPrompt string) string {
88+
prompt := promptui.Prompt{
89+
Label: "Edit Prompt",
90+
Default: defaultPrompt,
91+
AllowEdit: true,
92+
}
93+
result, err := prompt.Run()
94+
if err != nil {
95+
return defaultPrompt
96+
}
97+
return result
98+
}

config/config.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// File: config/config.go
2+
package config
3+
4+
import (
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"gopkg.in/yaml.v3"
11+
)
12+
13+
type Config struct {
14+
Provider string `yaml:"provider"`
15+
Model string `yaml:"model"`
16+
APIKey string `yaml:"api_key"`
17+
SystemPrompt string `yaml:"system_prompt"`
18+
}
19+
20+
func defaultConfig() *Config {
21+
return &Config{
22+
Provider: "ollama",
23+
Model: "llama3",
24+
SystemPrompt: "You are a terse, developer-friendly AI who writes conventional commit messages. Prefer single-line messages like 'feat: add ...' or 'fix: resolve ...'. Avoid PR-style summaries, bullet points, also this is official commit message. JUST SHOW THE COMMIT MESSAGE, NO OTHER BS.",
25+
}
26+
}
27+
28+
func configPath() string {
29+
configDir, _ := os.UserConfigDir()
30+
full := filepath.Join(configDir, "smartcommit", "config.yaml")
31+
fmt.Println("🕵️ Config path being used:", full) // <- add this
32+
return full
33+
}
34+
35+
36+
func LoadOrDefault() *Config {
37+
path := configPath()
38+
data, err := os.ReadFile(path)
39+
if err != nil {
40+
return defaultConfig()
41+
}
42+
var cfg Config
43+
if err := yaml.Unmarshal(data, &cfg); err != nil {
44+
return defaultConfig()
45+
}
46+
return &cfg
47+
}
48+
49+
func Save(cfg *Config) error {
50+
path := configPath()
51+
os.MkdirAll(filepath.Dir(path), 0755)
52+
data, err := yaml.Marshal(cfg)
53+
if err != nil {
54+
return err
55+
}
56+
return os.WriteFile(path, data, 0644)
57+
}
58+
59+
func (cfg *Config) Set(key, value string) {
60+
switch strings.ToLower(key) {
61+
case "provider":
62+
cfg.Provider = value
63+
case "model":
64+
cfg.Model = value
65+
case "api_key":
66+
cfg.APIKey = value
67+
case "system_prompt":
68+
cfg.SystemPrompt = value
69+
}
70+
}
71+
72+
func (cfg *Config) PrettyPrint() {
73+
fmt.Println("Current config:")
74+
fmt.Println(" provider: ", cfg.Provider)
75+
fmt.Println(" model: ", cfg.Model)
76+
if cfg.APIKey != "" {
77+
fmt.Println(" api_key: ", mask(cfg.APIKey))
78+
}
79+
fmt.Println(" system_prompt: ", cfg.SystemPrompt)
80+
}
81+
82+
func mask(s string) string {
83+
if len(s) <= 6 {
84+
return "******"
85+
}
86+
return s[:3] + "..." + s[len(s)-3:]
87+
}

diff/git.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package diff
2+
3+
import (
4+
"os/exec"
5+
)
6+
7+
func GetStagedDiff() (string, error) {
8+
out, err := exec.Command("git", "diff", "--cached").Output()
9+
if err != nil {
10+
return "", err
11+
}
12+
return string(out), nil
13+
}

go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module smartcommit
2+
3+
go 1.24.5
4+
5+
require (
6+
github.com/manifoldco/promptui v0.9.0
7+
github.com/spf13/cobra v1.9.1
8+
gopkg.in/yaml.v3 v3.0.1
9+
)
10+
11+
require (
12+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
13+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
14+
github.com/spf13/pflag v1.0.6 // indirect
15+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
16+
)

go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
2+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
3+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
4+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
5+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
6+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
7+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
8+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
9+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
10+
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
11+
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
12+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
13+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
14+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
15+
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
16+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
17+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
18+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
19+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
20+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
22+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

llm/ollama.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package llm
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"net/http"
8+
)
9+
10+
type OllamaProvider struct {
11+
Model string
12+
}
13+
14+
func (o *OllamaProvider) Name() string {
15+
return "ollama"
16+
}
17+
18+
func (o *OllamaProvider) Generate(prompt string) (string, error) {
19+
payload := map[string]interface{}{
20+
"model": o.Model,
21+
"prompt": prompt,
22+
"stream": false,
23+
}
24+
25+
body, _ := json.Marshal(payload)
26+
27+
resp, err := http.Post("http://localhost:11434/api/generate", "application/json", bytes.NewBuffer(body))
28+
if err != nil {
29+
return "", err
30+
}
31+
defer resp.Body.Close()
32+
33+
var result struct {
34+
Response string `json:"response"`
35+
}
36+
37+
err = json.NewDecoder(resp.Body).Decode(&result)
38+
if err != nil {
39+
return "", err
40+
}
41+
if result.Response == "" {
42+
return "", errors.New("no response from model")
43+
}
44+
45+
return result.Response, nil
46+
}

llm/provider.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package llm
2+
3+
type Provider interface {
4+
Name() string
5+
Generate(prompt string) (string, error)
6+
}

0 commit comments

Comments
 (0)