jesseduffield / jesseduffield/lazygit

Cached PR title with leading newline can corrupt state.yml

Open
#5,549 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
82.4k
Forks
3k
Avg merge
2d 18h
Merged PRs (30d)
19

Description

### Describe the bug

Lazygit can write a generated `state.yml` that it cannot read back on the next launch when a cached GitHub pull request title begins with a newline.

Startup then fails before the UI opens with an error like:

```text
yaml: line 79: did not find expected key
```

The invalid state was in the generated GitHub PR cache under `githubPullRequests`:

```yaml
githubPullRequests:
/path/to/repo:
- headRefName: example-branch
number: 9
title: |4-

Example pull request title
state: MERGED
url: https://github.com/example/example/pull/9
headRepositoryOwner: example
```

Removing `~/.local/state/lazygit/state.yml` lets lazygit start again, but the problem can recur if the cache is written with the same title shape.

### To Reproduce

This reproduces with the same `gopkg.in/yaml.v3` marshal/unmarshal path used for app state:

```go
package main

import (
"fmt"

"gopkg.in/yaml.v3"
)

type State struct {
GithubPullRequests map[string][]PR `yaml:"githubPullRequests"`
}

type PR struct {
HeadRefName string `yaml:"headRefName"`
Number int `yaml:"number"`
Title string `yaml:"title"`
State string `yaml:"state"`
}

func main() {
state := State{GithubPullRequests: map[string][]PR{
"/tmp/repo": {{
HeadRefName: "example-branch",
Number: 9,
Title: "\nExample pull request title",
State: "MERGED",
}},
}}

b, err := yaml.Marshal(state)
fmt.Println(err)
fmt.Print(string(b))

var out State
fmt.Println(yaml.Unmarshal(b, &out))
}
```

Output:

```text

githubPullRequests:
/tmp/repo:
- headRefName: example-branch
number: 9
title: |4-
Example pull request title
state: MERGED
yaml: line 2: did not find expected key
```

I also reproduced this against current `master` with a small regression test that:

1. Creates an `AppState` with a `CachedPullRequest.Title` beginning with `\n`.
2. Marshals it with `yaml.Marshal`.
3. Unmarshals it back into `AppState`.

The unmarshal fails.

### Expected behavior

Lazygit should not persist generated app state that prevents the next launch.

A possible fix is to normalize cached PR titles before app state serialization, or otherwise avoid the `yaml.v3` leading-newline block scalar output for this field.

### Version info

```text
commit=v0.61.1, build date=2026-04-13T21:15:08Z, build source=binaryRelease, version=0.61.1, os=linux, arch=amd64, git version=2.54.0
```

I also checked current latest release, which is `v0.61.1`.

### Terminal info

Linux shell/tmux environment.

This does not appear to be terminal-rendering related; startup fails while parsing `state.yml`.

### Additional context

The path involved appears to be:

- `pkg/gui/controllers/helpers/refresh_helper.go` copies `pr.Title` into `config.CachedPullRequest.Title`
- `pkg/config/app_config.go` writes `AppState` using `yaml.Marshal(c.appState)`
- the next launch reads that file using `yaml.Unmarshal(appStateBytes, appState)`

I tested one small local patch that made the regression test pass:

```go
func (c CachedPullRequest) MarshalYAML() (interface{}, error) {
type cachedPullRequest CachedPullRequest
result := cachedPullRequest(c)
result.Title = strings.TrimLeft(result.Title, "\r\n")
return result, nil
}
```

That may not be the preferred upstream fix, but it prevents this specific cache field from making `state.yml` unreadable.

I saw the current contributing note about opening an issue before proposing a PR. Happy to provide the regression test/patch if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.