Repository: jesseduffield/lazygit (language: go)

Issue:
Conflict during merge, lost last line of code after resolving conflict
**Describe the bug**
Conflict during merge, lost last line of code after resolving conflict

**To Reproduce**

1. merge code
2. resolving conflict

**Screenshots**

![image](https://github.com/jesseduffield/lazygit/assets/19773374/ed7f8a04-c25c-4579-8ad7-045500882a5b)
![image](https://github.com/jesseduffield/lazygit/assets/19773374/321073ba-ea85-43d0-8ee2-40992bb4db7c)
![image](https://github.com/jesseduffield/lazygit/assets/19773374/5088e220-8feb-4cdc-bfda-01cad80a4503)
![image](https://github.com/jesseduffield/lazygit/assets/19773374/d8668c3d-d4a6-4a30-9d16-fe930a931605)


**Version info:**
`lazygit --version`
commit=3675570a391b1a49ddd198b4c7e71e17701d4404, build date=2024-03-23T09:09:11Z, build source=binaryRelease, version=0.41.0, os=linux, arch=amd64, git version=2.33.1
 `git --version`
git version 2.33.1

`uname -a`
Linux my 6.1.32-amd64-desktop-hwe #23.01.00.20 SMP PREEMPT_DYNAMIC Thu Jan 11 18:45:10 CST 2024 x86_64 GNU/Linux




Discussion / hints:
same here

+1
+1
+1
+1

Fix patch:
diff --git a/pkg/utils/io.go b/pkg/utils/io.go
index d31c5fc073e..1d222746b95 100644
--- a/pkg/utils/io.go
+++ b/pkg/utils/io.go
@@ -2,6 +2,7 @@ package utils
 
 import (
 	"bufio"
+	"io"
 	"os"
 )
 
@@ -12,14 +13,18 @@ func ForEachLineInFile(path string, f func(string, int)) error {
 	}
 	defer file.Close()
 
-	reader := bufio.NewReader(file)
+	forEachLineInStream(file, f)
+
+	return nil
+}
+
+func forEachLineInStream(reader io.Reader, f func(string, int)) {
+	bufferedReader := bufio.NewReader(reader)
 	for i := 0; true; i++ {
-		line, err := reader.ReadString('\n')
-		if err != nil {
+		line, _ := bufferedReader.ReadString('\n')
+		if len(line) == 0 {
 			break
 		}
 		f(line, i)
 	}
-
-	return nil
 }


Test patch:
diff --git a/pkg/integration/tests/conflicts/resolve_without_trailing_lf.go b/pkg/integration/tests/conflicts/resolve_without_trailing_lf.go
new file mode 100644
index 00000000000..2af1eac5445
--- /dev/null
+++ b/pkg/integration/tests/conflicts/resolve_without_trailing_lf.go
@@ -0,0 +1,57 @@
+package conflicts
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var ResolveWithoutTrailingLf = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Regression test for resolving a merge conflict when the file doesn't have a trailing newline",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			NewBranch("branch1").
+			CreateFileAndAdd("file", "a\n\nno eol").
+			Commit("initial commit").
+			UpdateFileAndAdd("file", "a1\n\nno eol").
+			Commit("commit on branch1").
+			NewBranchFrom("branch2", "HEAD^").
+			UpdateFileAndAdd("file", "a2\n\nno eol").
+			Commit("commit on branch2").
+			Checkout("branch1").
+			RunCommandExpectError([]string{"git", "merge", "--no-edit", "branch2"})
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			IsFocused().
+			Lines(
+				Contains("UU file").IsSelected(),
+			).
+			PressEnter()
+
+		t.Views().MergeConflicts().
+			IsFocused().
+			SelectedLines(
+				Contains("<<<<<<< HEAD"),
+				Contains("a1"),
+				Contains("======="),
+			).
+			SelectNextItem().
+			PressPrimaryAction()
+
+		t.ExpectPopup().Alert().
+			Title(Equals("Continue")).
+			Content(Contains("All merge conflicts resolved. Continue?")).
+			Cancel()
+
+		t.Views().Files().
+			Focus().
+			Lines(
+				Contains("M  file").IsSelected(),
+			)
+
+		t.Views().Main().Content(Contains("-a1\n+a2\n").DoesNotContain("-no eol"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 6c6d484cf75..0c0142e40a9 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -123,6 +123,7 @@ var tests = []*components.IntegrationTest{
 	conflicts.ResolveExternally,
 	conflicts.ResolveMultipleFiles,
 	conflicts.ResolveNoAutoStage,
+	conflicts.ResolveWithoutTrailingLf,
 	conflicts.UndoChooseHunk,
 	custom_commands.AccessCommitProperties,
 	custom_commands.BasicCommand,
diff --git a/pkg/utils/io_test.go b/pkg/utils/io_test.go
new file mode 100644
index 00000000000..b8dfa957c43
--- /dev/null
+++ b/pkg/utils/io_test.go
@@ -0,0 +1,57 @@
+package utils
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_forEachLineInStream(t *testing.T) {
+	scenarios := []struct {
+		name          string
+		input         string
+		expectedLines []string
+	}{
+		{
+			name:          "empty input",
+			input:         "",
+			expectedLines: []string{},
+		},
+		{
+			name:          "single line",
+			input:         "abc\n",
+			expectedLines: []string{"abc\n"},
+		},
+		{
+			name:          "single line without line feed",
+			input:         "abc",
+			expectedLines: []string{"abc"},
+		},
+		{
+			name:          "multiple lines",
+			input:         "abc\ndef\n",
+			expectedLines: []string{"abc\n", "def\n"},
+		},
+		{
+			name:          "multiple lines including empty lines",
+			input:         "abc\n\ndef\n",
+			expectedLines: []string{"abc\n", "\n", "def\n"},
+		},
+		{
+			name:          "multiple lines without linefeed at end of file",
+			input:         "abc\ndef\nghi",
+			expectedLines: []string{"abc\n", "def\n", "ghi"},
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.name, func(t *testing.T) {
+			lines := []string{}
+			forEachLineInStream(strings.NewReader(s.input), func(line string, i int) {
+				lines = append(lines, line)
+			})
+			assert.EqualValues(t, s.expectedLines, lines)
+		})
+	}
+}