Skip to content

Commit 4e361e1

Browse files
authored
Fix merge conflict resolution when file doesn't end with a LF (jesseduffield#3976)
- **PR Description** When resolving conflicts using lazygit's merge conflicts view in a file that doesn't end with a trailing line feed, the last line would be lost. Fixes jesseduffield#3444. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
2 parents d11e11d + 696e78f commit 4e361e1

File tree

4 files changed

+125
-5
lines changed

4 files changed

+125
-5
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package conflicts
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var ResolveWithoutTrailingLf = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Regression test for resolving a merge conflict when the file doesn't have a trailing newline",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.
15+
NewBranch("branch1").
16+
CreateFileAndAdd("file", "a\n\nno eol").
17+
Commit("initial commit").
18+
UpdateFileAndAdd("file", "a1\n\nno eol").
19+
Commit("commit on branch1").
20+
NewBranchFrom("branch2", "HEAD^").
21+
UpdateFileAndAdd("file", "a2\n\nno eol").
22+
Commit("commit on branch2").
23+
Checkout("branch1").
24+
RunCommandExpectError([]string{"git", "merge", "--no-edit", "branch2"})
25+
},
26+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
27+
t.Views().Files().
28+
IsFocused().
29+
Lines(
30+
Contains("UU file").IsSelected(),
31+
).
32+
PressEnter()
33+
34+
t.Views().MergeConflicts().
35+
IsFocused().
36+
SelectedLines(
37+
Contains("<<<<<<< HEAD"),
38+
Contains("a1"),
39+
Contains("======="),
40+
).
41+
SelectNextItem().
42+
PressPrimaryAction()
43+
44+
t.ExpectPopup().Alert().
45+
Title(Equals("Continue")).
46+
Content(Contains("All merge conflicts resolved. Continue?")).
47+
Cancel()
48+
49+
t.Views().Files().
50+
Focus().
51+
Lines(
52+
Contains("M file").IsSelected(),
53+
)
54+
55+
t.Views().Main().Content(Contains("-a1\n+a2\n").DoesNotContain("-no eol"))
56+
},
57+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ var tests = []*components.IntegrationTest{
123123
conflicts.ResolveExternally,
124124
conflicts.ResolveMultipleFiles,
125125
conflicts.ResolveNoAutoStage,
126+
conflicts.ResolveWithoutTrailingLf,
126127
conflicts.UndoChooseHunk,
127128
custom_commands.AccessCommitProperties,
128129
custom_commands.BasicCommand,

pkg/utils/io.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package utils
22

33
import (
44
"bufio"
5+
"io"
56
"os"
67
)
78

@@ -12,14 +13,18 @@ func ForEachLineInFile(path string, f func(string, int)) error {
1213
}
1314
defer file.Close()
1415

15-
reader := bufio.NewReader(file)
16+
forEachLineInStream(file, f)
17+
18+
return nil
19+
}
20+
21+
func forEachLineInStream(reader io.Reader, f func(string, int)) {
22+
bufferedReader := bufio.NewReader(reader)
1623
for i := 0; true; i++ {
17-
line, err := reader.ReadString('\n')
18-
if err != nil {
24+
line, _ := bufferedReader.ReadString('\n')
25+
if len(line) == 0 {
1926
break
2027
}
2128
f(line, i)
2229
}
23-
24-
return nil
2530
}

pkg/utils/io_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package utils
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func Test_forEachLineInStream(t *testing.T) {
11+
scenarios := []struct {
12+
name string
13+
input string
14+
expectedLines []string
15+
}{
16+
{
17+
name: "empty input",
18+
input: "",
19+
expectedLines: []string{},
20+
},
21+
{
22+
name: "single line",
23+
input: "abc\n",
24+
expectedLines: []string{"abc\n"},
25+
},
26+
{
27+
name: "single line without line feed",
28+
input: "abc",
29+
expectedLines: []string{"abc"},
30+
},
31+
{
32+
name: "multiple lines",
33+
input: "abc\ndef\n",
34+
expectedLines: []string{"abc\n", "def\n"},
35+
},
36+
{
37+
name: "multiple lines including empty lines",
38+
input: "abc\n\ndef\n",
39+
expectedLines: []string{"abc\n", "\n", "def\n"},
40+
},
41+
{
42+
name: "multiple lines without linefeed at end of file",
43+
input: "abc\ndef\nghi",
44+
expectedLines: []string{"abc\n", "def\n", "ghi"},
45+
},
46+
}
47+
48+
for _, s := range scenarios {
49+
t.Run(s.name, func(t *testing.T) {
50+
lines := []string{}
51+
forEachLineInStream(strings.NewReader(s.input), func(line string, i int) {
52+
lines = append(lines, line)
53+
})
54+
assert.EqualValues(t, s.expectedLines, lines)
55+
})
56+
}
57+
}

0 commit comments

Comments
 (0)