Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs-master/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,28 @@ git:
# If autoWrapCommitMessage is true, the width to wrap to
autoWrapWidth: 72

# AI commit message generation config
ai:
# CLI-based generation: runs a command with the staged diff piped to stdin.
# Example: "claude -p 'Generate a conventional commit message for this diff:'"
cli:
# The shell command to run. The staged diff is piped to stdin.
command: ""

# OpenAI-compatible API generation
api:
# OpenAI-compatible base URL, e.g. "https://api.openai.com/v1"
endpoint: ""

# Model name, e.g. "gpt-4o" or "llama3"
model: ""

# API key value. Use {env:MY_API_KEY} to read from an environment variable.
apiKey: ""

# System prompt. Defaults to a conventional commit prompt if omitted.
systemPrompt: ""
Comment on lines +362 to +382
Copy link
Copy Markdown
Contributor

@ruudk ruudk Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I don't think we should make the integration like this. All we need is a command that can be triggered to generate a commit. Then the user can configure it however they want. No need for custom integrations and vendor specific things.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate a PR.

I hope you mean generate a commit . And also.. if I get this right, you're saying:

  • ai.cli.command - is good
  • ai.api fields - is bad because lazygit shouldn't have integrations like this.

I agree I think it'll keep this PR a lot leaner. And less burden on lazygit.

      # OpenAI-compatible API generation
      api:
        # OpenAI-compatible base URL, e.g. "https://api.openai.com/v1"
        endpoint: ""

        # Model name, e.g. "gpt-4o" or "llama3"
        model: ""

        # API key value. Use {env:MY_API_KEY} to read from an environment variable.
        apiKey: ""

        # System prompt. Defaults to a conventional commit prompt if omitted.
        systemPrompt: ""

I thought of adding it because people might just wanna hook up their own ai integrations as quickly as possible without installing a coding agent / anything. If that's the case, there should probably be a recommended minimal cli used alongside here just for this purpose. The closest I know is (https://github.com/simonw/llm). But tbh I think I could just make one in rust.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I meant commit indeed.

Yeah I have the feeling this will never be accepted with all that custom logic. So the simpler the change will be, the better.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made that minimal CLI: https://github.com/Blankeos/modelcli

I'll go ahead and tweak an alternate PR with those minimal changes.


# Config relating to merging
merging:
# If true, run merges in a subprocess so that if a commit message is required,
Expand Down
22 changes: 22 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ If you want to change the config directory:

- MacOS: `export XDG_CONFIG_HOME="$HOME/.config"`

You can place a `.env` file in the same directory as your `config.yml` to define secrets (e.g. API keys) outside of version control. Reference them anywhere in your config using the `{env:VARIABLE_NAME}` syntax — for example, `apiKey: "{env:OPENAI_API_KEY}"`. Variables already set in your shell take precedence over values in `.env`.

In addition to the global config file you can create repo-specific config files in `<repo>/.git/lazygit.yml`. Settings in these files override settings in the global config file. In addition, files called `.lazygit.yml` in any of the parent directories of a repo will also be loaded; this can be useful if you have settings that you want to apply to a group of repositories.

JSON schema is available for `config.yml` so that IntelliSense in Visual Studio Code (completion and error checking) is automatically enabled when the [YAML Red Hat][yaml] extension is installed. However, note that automatic schema detection only works if your config file is in one of the standard paths mentioned above. If you override the path to the file, you can still make IntelliSense work by adding
Expand Down Expand Up @@ -359,6 +361,26 @@ git:
# If autoWrapCommitMessage is true, the width to wrap to
autoWrapWidth: 72

# Config for AI-generated commit messages (accessible via the commit menu with <c-o> then 'a')
# Use either the 'cli' or 'api' approach, not both.
ai:
# Use a local CLI tool (e.g. claude, opencode, llm) to generate commit messages.
# The staged diff is piped to stdin; the generated message is read from stdout.
# Markdown code fences in the output are stripped automatically.
# Example: 'claude -p "Generate a conventional commit message:"'
cli:
command: 'claude -p "Generated a conventional commit message for this diff"'

# Use an OpenAI-compatible HTTP API to generate commit messages.
# Use {env:MY_VAR} to read the API key from an environment variable or
# from the .env file next to config.yml (see above).
api:
endpoint: "https://api.openai.com/v1/chat/completions"
model: "gpt-4o-mini"
apiKey: "{env:OPENAI_API_KEY}"
# Optional system prompt. A sensible default is used when left empty.
systemPrompt: ""

# Config relating to merging
merging:
# If true, run merges in a subprocess so that if a commit message is required,
Expand Down
43 changes: 43 additions & 0 deletions pkg/config/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -82,6 +83,8 @@ func NewAppConfig(
return nil, err
}

loadDotEnv(filepath.Join(configDir, ".env"))

var configFiles []*ConfigFile
customConfigFiles := os.Getenv("LG_CONFIG_FILE")
if customConfigFiles != "" {
Expand Down Expand Up @@ -135,6 +138,45 @@ func findOrCreateConfigDir() (string, error) {
return folder, os.MkdirAll(folder, 0o755)
}

var envInterpolatePattern = regexp.MustCompile(`\{env:([^}]+)\}`)

// envInterpolate replaces {env:KEY} placeholders in config content with the
// corresponding environment variable values.
func envInterpolate(content []byte) []byte {
return envInterpolatePattern.ReplaceAllFunc(content, func(match []byte) []byte {
key := string(match[5 : len(match)-1]) // strip "{env:" and "}"
return []byte(os.Getenv(key))
})
}

// loadDotEnv reads a .env file and sets any key=value pairs as environment
// variables, skipping keys that are already set. Lines starting with '#' and
// blank lines are ignored. Quoted values have their quotes stripped.
func loadDotEnv(path string) {
data, err := os.ReadFile(path)
if err != nil {
return // file is optional
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if len(value) >= 2 && (value[0] == '"' || value[0] == '\'') && value[len(value)-1] == value[0] {
value = value[1 : len(value)-1]
}
if os.Getenv(key) == "" {
_ = os.Setenv(key, value)
}
}
}

func loadUserConfigWithDefaults(configFiles []*ConfigFile, isGuiInitialized bool) (*UserConfig, error) {
return loadUserConfig(configFiles, GetDefaultConfig(), isGuiInitialized)
}
Expand Down Expand Up @@ -191,6 +233,7 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig, isGuiInitialize

existingCustomCommands := base.CustomCommands

content = envInterpolate(content)
if err := yaml.Unmarshal(content, base); err != nil {
return nil, fmt.Errorf("The config at `%s` couldn't be parsed, please inspect it before opening up an issue.\n%w", path, err)
}
Expand Down
38 changes: 38 additions & 0 deletions pkg/config/app_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,44 @@ import (
"github.com/stretchr/testify/assert"
)

func TestEnvInterpolate(t *testing.T) {
t.Setenv("MY_KEY", "hello")
t.Setenv("ANOTHER", "world")

scenarios := []struct {
name string
input string
expected string
}{
{
name: "no placeholders unchanged",
input: "apiKey: hardcoded",
expected: "apiKey: hardcoded",
},
{
name: "single placeholder replaced",
input: "apiKey: {env:MY_KEY}",
expected: "apiKey: hello",
},
{
name: "multiple placeholders replaced",
input: "{env:MY_KEY} and {env:ANOTHER}",
expected: "hello and world",
},
{
name: "unset variable replaced with empty string",
input: "apiKey: {env:UNSET_VAR_XYZ}",
expected: "apiKey: ",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := envInterpolate([]byte(s.input))
assert.Equal(t, s.expected, string(result))
})
}
}

func TestMigrationOfRenamedKeys(t *testing.T) {
scenarios := []struct {
name string
Expand Down
26 changes: 26 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,32 @@ type CommitConfig struct {
AutoWrapCommitMessage bool `yaml:"autoWrapCommitMessage"`
// If autoWrapCommitMessage is true, the width to wrap to
AutoWrapWidth int `yaml:"autoWrapWidth"`
// AI commit message generation config
AI AIConfig `yaml:"ai"`
}

type AIConfig struct {
// CLI-based generation: runs a command with the staged diff piped to stdin.
// Example: "claude -p 'Generate a conventional commit message for this diff:'"
CLI AICliConfig `yaml:"cli"`
// OpenAI-compatible API generation
API AIAPIConfig `yaml:"api"`
}

type AICliConfig struct {
// The shell command to run. The staged diff is piped to stdin.
Command string `yaml:"command"`
}

type AIAPIConfig struct {
// OpenAI-compatible base URL, e.g. "https://api.openai.com/v1"
Endpoint string `yaml:"endpoint"`
// Model name, e.g. "gpt-4o" or "llama3"
Model string `yaml:"model"`
// API key value. Use {env:MY_API_KEY} to read from an environment variable.
APIKey string `yaml:"apiKey"`
// System prompt. Defaults to a conventional commit prompt if omitted.
SystemPrompt string `yaml:"systemPrompt"`
}

type MergingConfig struct {
Expand Down
8 changes: 8 additions & 0 deletions pkg/gui/controllers/commit_message_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
}

func (self *CommitMessageController) confirm() error {
if self.c.Helpers().Commits.IsGenerating() {
return nil
}

// The default keybinding for this action is "<enter>", which means that we
// also get here when pasting multi-line text that contains newlines. In
// that case we don't want to confirm the commit, but switch to the
Expand All @@ -185,6 +189,10 @@ func (self *CommitMessageController) confirm() error {
}

func (self *CommitMessageController) close() error {
if self.c.Helpers().Commits.IsGenerating() {
return nil
}

self.c.Helpers().Commits.CloseCommitMessagePanel()
return nil
}
Expand Down
Loading