Repository: jesseduffield/lazygit (language: go)

Issue:
Lazygit hangs terminal on listing commits
**Describe the bug**
I have a weird issue that Lazygit does not list the commits in the `[4] Commits` window (but it lists them in the `Log` graph window on the right hand side - see screenshot below). Moreover, when trying to do any operation on that branch, like trying to checkout different branch or list the commits by using <Enter> in `[3] Local branches` view, the terminal hangs (in a way it has to be killed entirely).

The weird thing about this is that it is happening only in one particular repository on one particular branch (**and** any branches created from it). It doesn't happen for the same repo with a branch not derived from the `develop` (the problematic branch).

It started somewhat after updating lazygit from 0.40.x but I tried downgrading to 0.40.2 and the same happens. At this point I'm clueless and desperate as it really messed up my workflow.. Please help, any ideas are appreciated.

**Testing**
I tried to
- re-clone the repo
- run `git fsck` (no errors)
- browse through commits until a merge-base of working branch (master) - nothing weird
- reconfigured showUntrackedFiles
- clean the .git objects and reset, update index
- run all commands from `lazygit --debug` manually in the terminal. Nothing seems to return any error or hang the terminal
- upgrade lazygit to latest build from master
- go back to 0.40.2

Below is the debug log output from starting `lazygit` to issuing <Enter> on `[3] Local branches`, which hangs the terminal:

```
Apr 26 19:21:17 |INFO| Receiving focus - refreshing
Apr 26 19:21:17 |INFO| refreshing all scopes in async mode
Apr 26 19:21:17 |INFO| Refresh took 105.474µs
Apr 26 19:21:17 |DEBU| using cache for key status.showUntrackedFiles
Apr 26 19:21:17 |DEBU| RunCommand command="git stash list -z --pretty=%ct|%gs"
Apr 26 19:21:17 |DEBU| RunCommand command="git merge-base refs/heads/develop develop@{u}"
Apr 26 19:21:17 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z"
Apr 26 19:21:17 |DEBU| RunCommand command="git tag --list -n --sort=-creatordate"
Apr 26 19:21:17 |DEBU| RunCommand command="git merge-base refs/heads/develop refs/remotes/origin/master"
Apr 26 19:21:17 |INFO| git stash list -z --pretty=%ct|%gs (1.983116ms)
Apr 26 19:21:17 |INFO| postRefreshUpdate for stash took 69.284µs
Apr 26 19:21:17 |INFO| git merge-base refs/heads/develop develop@{u} (2.189666ms)
Apr 26 19:21:17 |INFO| git -c log.showSignature=false log -g --abbrev=40 --format=%h%x00%ct%x00%gs%x00%p (2.617862ms)
Apr 26 19:21:17 |INFO| postRefreshUpdate for reflogCommits took 2.170871ms
Apr 26 19:21:17 |INFO| git tag --list -n --sort=-creatordate (5.262017ms)
Apr 26 19:21:17 |INFO| git for-each-ref --sort=-committerdate --format=%(refname:short) refs/remotes (5.744ms)
Apr 26 19:21:17 |INFO| postRefreshUpdate for remotes took 149.964µs
Apr 26 19:21:17 |INFO| postRefreshUpdate for remoteBranches took 24.894µs
Apr 26 19:21:17 |INFO| git merge-base refs/heads/develop refs/remotes/origin/master (8.076531ms)
Apr 26 19:21:17 |INFO| postRefreshUpdate for tags took 7.205243ms
Apr 26 19:21:17 |INFO| git status --untracked-files=all --porcelain -z (105.071484ms)
Apr 26 19:21:23 |INFO| refreshing the following scopes in sync mode: files
Apr 26 19:21:23 |INFO| refreshed merge conflicts in 13.297µs
Apr 26 19:21:23 |DEBU| using cache for key status.showUntrackedFiles
Apr 26 19:21:23 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z"
Apr 26 19:21:23 |INFO| git status --untracked-files=all --porcelain -z (98.220525ms)
Apr 26 19:21:23 |INFO| refreshed files in 98.665017ms
Apr 26 19:21:23 |INFO| Refresh took 99.019232ms
Apr 26 19:21:33 |INFO| refreshing the following scopes in sync mode: files
Apr 26 19:21:33 |INFO| refreshed merge conflicts in 4.099µs
Apr 26 19:21:33 |DEBU| using cache for key status.showUntrackedFiles
Apr 26 19:21:33 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z"
Apr 26 19:21:33 |INFO| git status --untracked-files=all --porcelain -z (80.626224ms)
Apr 26 19:21:33 |INFO| refreshed files in 81.263667ms
Apr 26 19:21:33 |INFO| Refresh took 81.691062ms
```

**To Reproduce**
No clue how to reproduce that as it happens only on one branch on particular repository

**Expected behavior**
Commits are listed properly in `Commit` window, Lazygit does not hang the terminal when trying to enter branch commits or checkout another branch

**Screenshots**
![lazygit-issue](https://github.com/jesseduffield/lazygit/assets/25208694/0390705f-c055-4887-9d18-0f83168a5696)

**Version info:**
_commit=aa81e191e2905e6959f1d8e51c8589764ca0c520, build date=2024-04-25T06:53:14Z, build source=unknown, version=aa81e191, os=linux, arch=amd64, git version=2.43.2_
Ubuntu 22.04 on WSL2



Discussion / hints:
I don't suppose you can share that repo?
I'm afraid not, unfortunately. 
Ok, then it's going to be a bit more cumbersome to figure this out...

My suspicion is that it has something to do with the git log command that lazygit issues to load the commits. I don't see this in the log that you posted; look for a line that has `-300` in it. Can you issue that command in the terminal (when the branch is checked out) and see if anything looks unusual about it?
I don't see any lines starting with `-300`. However, when starting the `--logs` first and then lazygit debug, I can see couple errors on startup, but doesn't seem related.

Errors:
```
Apr 26 21:51:33 |DEBU| Error getting git config value for key: rebase.updateRefs. Error: the key is not found for [git config --get --null rebase.updateRefs]
Apr 26 21:51:33 |ERRO| fatal: no such branch: 'main'
 command="git rev-parse --symbolic-full-name main@{u}"
Apr 26 21:51:33 |ERRO| <no msg> command="git rev-parse --verify --quiet refs/remotes/origin/main"
Apr 26 21:51:33 |ERRO| <no msg> command="git rev-parse --verify --quiet refs/heads/main"
```

Not sure why it is trying to reach out for `main` branch, as its not there. After creating `main` branch on both local and remote the errors are gone, except the `updateRefs` one.

Full log:
```
Apr 26 21:51:33 |INFO| language: C
Apr 26 21:51:33 |DEBU| RunCommand command="git --version"
Apr 26 21:51:33 |INFO| git --version (2.095629ms)
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --git-dir"
Apr 26 21:51:33 |INFO| git rev-parse --git-dir (4.186858ms)
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --is-bare-repository"
Apr 26 21:51:33 |INFO| git rev-parse --is-bare-repository (3.135544ms)
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --path-format=absolute --show-toplevel --absolute-git-dir --git-common-dir --show-superproject-working-tree"
Apr 26 21:51:33 |INFO| git rev-parse --path-format=absolute --show-toplevel --absolute-git-dir --git-common-dir --show-superproject-working-tree (4.938524ms)
Apr 26 21:51:33 |INFO| starting main loop
Apr 26 21:51:33 |INFO| Binary is not built with the buildBinary flag so we won't check for an update
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --is-bare-repository"
Apr 26 21:51:33 |DEBU| RunCommand command="git fetch --all --no-write-fetch-head"
Apr 26 21:51:33 |INFO| git rev-parse --is-bare-repository (2.220506ms)
Apr 26 21:51:33 |INFO| refreshing all scopes in async mode
Apr 26 21:51:33 |INFO| Refresh took 213.962µs
Apr 26 21:51:33 |DEBU| RunCommand command="git for-each-ref --sort=-committerdate --format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)%00%(subject)%00%(objectname)%00%(committerdate:unix) refs/heads"
Apr 26 21:51:33 |DEBU| RunCommand command="git tag --list -n --sort=-creatordate"
Apr 26 21:51:33 |DEBU| RunCommand command="git stash list -z --pretty=%ct|%gs"
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --abbrev-ref --verify HEAD"
Apr 26 21:51:33 |DEBU| Error getting git config value for key: rebase.updateRefs. Error: the key is not found for [git config --get --null rebase.updateRefs]
Apr 26 21:51:33 |INFO| git stash list -z --pretty=%ct|%gs (2.025941ms)
Apr 26 21:51:33 |INFO| git rev-parse --abbrev-ref --verify HEAD (1.981649ms)
Apr 26 21:51:33 |INFO| postRefreshUpdate for stash took 216.862µs
Apr 26 21:51:33 |DEBU| RunCommand command="git merge-base develop develop@{u}"
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --symbolic-full-name main@{u}"
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --symbolic-full-name master@{u}"
Apr 26 21:51:33 |ERRO| fatal: no such branch: 'main'
 command="git rev-parse --symbolic-full-name main@{u}"
Apr 26 21:51:33 |INFO| git rev-parse --symbolic-full-name main@{u} (1.893664ms)
Apr 26 21:51:33 |INFO| git rev-parse --symbolic-full-name master@{u} (1.878067ms)
Apr 26 21:51:33 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z"
Apr 26 21:51:33 |INFO| git merge-base develop develop@{u} (3.21383ms)
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --verify --quiet refs/remotes/origin/main"
Apr 26 21:51:33 |INFO| git tag --list -n --sort=-creatordate (7.127036ms)
Apr 26 21:51:33 |ERRO| <no msg> command="git rev-parse --verify --quiet refs/remotes/origin/main"
Apr 26 21:51:33 |INFO| git rev-parse --verify --quiet refs/remotes/origin/main (3.863615ms)
Apr 26 21:51:33 |DEBU| RunCommand command="git rev-parse --verify --quiet refs/heads/main"
Apr 26 21:51:33 |INFO| git for-each-ref --sort=-committerdate --format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)%00%(subject)%00%(objectname)%00%(committerdate:unix) refs/heads (13.19516ms)
Apr 26 21:51:33 |INFO| postRefreshUpdate for tags took 5.830665ms
Apr 26 21:51:33 |DEBU| RunCommand command="git worktree list --porcelain"
Apr 26 21:51:33 |INFO| git for-each-ref --sort=refname --format=%(refname:short) refs/remotes (19.559031ms)
Apr 26 21:51:33 |INFO| git worktree list --porcelain (6.919873ms)
Apr 26 21:51:33 |ERRO| <no msg> command="git rev-parse --verify --quiet refs/heads/main"
Apr 26 21:51:33 |INFO| git rev-parse --verify --quiet refs/heads/main (9.638591ms)
Apr 26 21:51:33 |INFO| postRefreshUpdate for remotes took 664.982µs
Apr 26 21:51:33 |INFO| postRefreshUpdate for remoteBranches took 28.195µs
Apr 26 21:51:33 |DEBU| RunCommand command="git -C /home/eryk/projects/anywhere/anywhere rev-parse --path-format=absolute --absolute-git-dir"
Apr 26 21:51:33 |DEBU| RunCommand command="git merge-base HEAD refs/remotes/origin/master"
Apr 26 21:51:33 |INFO| git -C /home/eryk/projects/anywhere/anywhere rev-parse --path-format=absolute --absolute-git-dir (4.309935ms)
Apr 26 21:51:33 |INFO| postRefreshUpdate for worktrees took 829.253µs
Apr 26 21:51:33 |INFO| postRefreshUpdate for localBranches took 1.549926ms
```
The errors that you see in the log are normal, nothing to worry about. And no, you don't need a `main` branch. We should find a way to avoid logging these errors, as it confuses people who are looking for problems.

The reason why you don't see the git log command seems to be because we only log it once it has completed, so from that I can only conclude that this command hangs for some reason. Can you try running this in the terminal:

```
git log HEAD --topo-order --oneline --pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m --abbrev=40 -300 --no-show-signature -- | cat
```
Ok, so I think I found the issue thanks to the command above.

First, I have `cat` aliased to `bat` and running the above gives me 

```
❯ g log HEAD --topo-order --oneline --pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m --abbrev=40 -300 --no-show-signature -- | cat
       │ STDIN   <BINARY>
```

Using unaliased `\cat` gives the proper commit list. However, this doesn't seem to be the issue I believe.

Proper commit list revealed, that in one commit, when converting `pom.xml` line endings, git (or someone accidentally) put the whole file diff in the description field.

```
diff --git a/pom.xml b/pom.xml
index 666b6208b..7e58aa731 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,1598 +1,1598 @@
-<?xml version="1.0"?>
-<!--

(...)
```

When I rebased the branch and removed the problematic commit, it started working. However, I'm not sure I can force push to that repo. Strange that no other git tool / command finds that output problematic. Maybe there is a way to escape the commit descriptions in lazygit?

Also maybe a good idea to figure out the actual unaliased `cat` command so the output is consistent (someone can alias `cat` with additional switches maybe, or use `bat` like me)
We are not actually using `cat` in lazygit, I only added this to the command line to simulate the environment that the command is run in by lazygit (with output going to a pipe instead of a tty). Without that, it would have presented the output in a pager, which might have changed things. You could have added `--no-pager` to the command line instead.

I'm not sure I understand the problem yet. You are talking about the description field, but the diff output you pasted only shows the diff, not the description, which doesn't seem to be relevant here.

Could you paste the output of
```
❯ git --no-pager log <hash-of-the-bad-commit> --oneline --pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m -1 --
```
> We are not actually using `cat` in lazygit, I only added this to the command line to simulate the environment that the command is run in by lazygit (with output going to a pipe instead of a tty).

Ooh, sure, didn't get that, sorry.

> You are talking about the description field, but the diff output you pasted only shows the diff, not the description

This **is** the commit description (the whole diff) :)

The output of the command:

```
0545af6c9d9e22f63bdbc4b355ab927dcdfaa3a11712302733someone@gmail.comdab515ab4fix: Convert pom.xml diff --git a/pom.xml b/pom.xml index 666b6208b..7e58aa731 100644 --- a/pom.xml +++ b/pom.xml @@ -1,1598 +1,1598 @@ -<?xml version="1.0"?> -<!-- -  ~ Copyright © 2021 -  --> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> (...)
```

And it goes for plethora of lines.



Fix patch:
diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go
index 48545edb953..3c01344d95a 100644
--- a/pkg/commands/git_commands/commit_loader.go
+++ b/pkg/commands/git_commands/commit_loader.go
@@ -211,11 +211,11 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
 	authorEmail := split[3]
 	extraInfo := strings.TrimSpace(split[4])
 	parentHashes := split[5]
-	message := split[6]
 	divergence := models.DivergenceNone
 	if showDivergence {
-		divergence = lo.Ternary(split[7] == "<", models.DivergenceLeft, models.DivergenceRight)
+		divergence = lo.Ternary(split[6] == "<", models.DivergenceLeft, models.DivergenceRight)
 	}
+	message := split[7]
 
 	tags := []string{}
 
@@ -605,4 +605,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
 	return self.cmd.New(cmdArgs).DontLog()
 }
 
-const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m`
+const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s`
diff --git a/pkg/commands/oscommands/cmd_obj_runner.go b/pkg/commands/oscommands/cmd_obj_runner.go
index 296850e3238..16257158e18 100644
--- a/pkg/commands/oscommands/cmd_obj_runner.go
+++ b/pkg/commands/oscommands/cmd_obj_runner.go
@@ -161,7 +161,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
 	}
 
 	scanner := bufio.NewScanner(stdoutPipe)
-	scanner.Split(bufio.ScanLines)
+	scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
 	if err := cmd.Start(); err != nil {
 		return err
 	}
@@ -178,6 +178,11 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
 		}
 	}
 
+	if scanner.Err() != nil {
+		_ = Kill(cmd)
+		return scanner.Err()
+	}
+
 	_ = cmd.Wait()
 
 	self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
diff --git a/pkg/gui/mergeconflicts/find_conflicts.go b/pkg/gui/mergeconflicts/find_conflicts.go
index 3802a66b7a8..c4d3a51a8b0 100644
--- a/pkg/gui/mergeconflicts/find_conflicts.go
+++ b/pkg/gui/mergeconflicts/find_conflicts.go
@@ -99,7 +99,7 @@ func FileHasConflictMarkers(path string) (bool, error) {
 // Efficiently scans through a file looking for merge conflict markers. Returns true if it does
 func fileHasConflictMarkersAux(file io.Reader) bool {
 	scanner := bufio.NewScanner(file)
-	scanner.Split(bufio.ScanLines)
+	scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
 	for scanner.Scan() {
 		line := scanner.Bytes()
 
diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go
index 202c3f23a68..e80b63a2a31 100644
--- a/pkg/tasks/tasks.go
+++ b/pkg/tasks/tasks.go
@@ -162,7 +162,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
 		done := make(chan struct{})
 
 		scanner := bufio.NewScanner(r)
-		scanner.Split(bufio.ScanLines)
+		scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))
 
 		lineChan := make(chan []byte)
 		lineWrittenChan := make(chan struct{})
diff --git a/pkg/utils/lines.go b/pkg/utils/lines.go
index 662ba2f9b4d..c70d02ffcef 100644
--- a/pkg/utils/lines.go
+++ b/pkg/utils/lines.go
@@ -1,6 +1,9 @@
 package utils
 
-import "strings"
+import (
+	"bytes"
+	"strings"
+)
 
 // SplitLines takes a multiline string and splits it on newlines
 // currently we are also stripping \r's which may have adverse effects for
@@ -43,3 +46,57 @@ func EscapeSpecialChars(str string) string {
 		"\v", "\\v",
 	).Replace(str)
 }
+
+func dropCR(data []byte) []byte {
+	if len(data) > 0 && data[len(data)-1] == '\r' {
+		return data[0 : len(data)-1]
+	}
+	return data
+}
+
+// ScanLinesAndTruncateWhenLongerThanBuffer returns a split function that can be
+// used with bufio.Scanner.Split(). It is very similar to bufio.ScanLines,
+// except that it will truncate lines that are longer than the scanner's read
+// buffer (whereas bufio.ScanLines will return an error in that case, which is
+// often difficult to handle).
+//
+// If you are using your own buffer for the scanner, you must set maxBufferSize
+// to the same value as the max parameter that you passed to scanner.Buffer().
+// Otherwise, maxBufferSize must be set to bufio.MaxScanTokenSize.
+func ScanLinesAndTruncateWhenLongerThanBuffer(maxBufferSize int) func(data []byte, atEOF bool) (int, []byte, error) {
+	skipOverRemainderOfLongLine := false
+
+	return func(data []byte, atEOF bool) (int, []byte, error) {
+		if atEOF && len(data) == 0 {
+			// Done
+			return 0, nil, nil
+		}
+		if i := bytes.IndexByte(data, '\n'); i >= 0 {
+			if skipOverRemainderOfLongLine {
+				skipOverRemainderOfLongLine = false
+				return i + 1, nil, nil
+			}
+			return i + 1, dropCR(data[0:i]), nil
+		}
+		if atEOF {
+			if skipOverRemainderOfLongLine {
+				return len(data), nil, nil
+			}
+
+			return len(data), dropCR(data), nil
+		}
+
+		// Buffer is full, so we can't get more data
+		if len(data) >= maxBufferSize {
+			if skipOverRemainderOfLongLine {
+				return len(data), nil, nil
+			}
+
+			skipOverRemainderOfLongLine = true
+			return len(data), data, nil
+		}
+
+		// Request more data.
+		return 0, nil, nil
+	}
+}


Test patch:
diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go
index 7f60209a4b5..d9aa8456cb0 100644
--- a/pkg/commands/git_commands/commit_loader_test.go
+++ b/pkg/commands/git_commands/commit_loader_test.go
@@ -15,16 +15,16 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode
-b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|fix logging
-e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|refactor
-d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
-65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
-26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
-3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
-053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
+var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode
+b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|>|fix logging
+e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|>|refactor
+d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|>|WIP
+65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|>|WIP
+26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|>|WIP
+3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|>|WIP
+053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|>|refactoring the config struct`, "|", "\x00", -1)
 
-var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
+var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode`, "|", "\x00", -1)
 
 func TestGetCommits(t *testing.T) {
 	type scenario struct {
@@ -46,7 +46,7 @@ func TestGetCommits(t *testing.T) {
 			opts:       GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
-				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
+				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
 
 			expectedCommits: []*models.Commit{},
 			expectedError:   nil,
@@ -58,7 +58,7 @@ func TestGetCommits(t *testing.T) {
 			opts:       GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
-				ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
+				ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
 
 			expectedCommits: []*models.Commit{},
 			expectedError:   nil,
@@ -73,7 +73,7 @@ func TestGetCommits(t *testing.T) {
 				// here it's seeing which commits are yet to be pushed
 				ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
 				// here it's actually getting all the commits in a formatted form, one per line
-				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
+				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
 				// here it's testing which of the configured main branches have an upstream
 				ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).       // this one does
 				ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).                   // this one doesn't, so it checks origin instead
@@ -210,7 +210,7 @@ func TestGetCommits(t *testing.T) {
 				// here it's seeing which commits are yet to be pushed
 				ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
 				// here it's actually getting all the commits in a formatted form, one per line
-				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
+				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
 				// here it's testing which of the configured main branches exist; neither does
 				ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
 				ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
@@ -247,7 +247,7 @@ func TestGetCommits(t *testing.T) {
 				// here it's seeing which commits are yet to be pushed
 				ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
 				// here it's actually getting all the commits in a formatted form, one per line
-				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
+				ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
 				// here it's testing which of the configured main branches exist
 				ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
 				ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
@@ -283,7 +283,7 @@ func TestGetCommits(t *testing.T) {
 			opts:       GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
-				ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
+				ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
 
 			expectedCommits: []*models.Commit{},
 			expectedError:   nil,
@@ -295,7 +295,7 @@ func TestGetCommits(t *testing.T) {
 			opts:       GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
 			runner: oscommands.NewFakeRunner(t).
 				ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
-				ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
+				ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
 
 			expectedCommits: []*models.Commit{},
 			expectedError:   nil,
diff --git a/pkg/utils/lines_test.go b/pkg/utils/lines_test.go
index e7171022b17..2192a3780ef 100644
--- a/pkg/utils/lines_test.go
+++ b/pkg/utils/lines_test.go
@@ -1,6 +1,8 @@
 package utils
 
 import (
+	"bufio"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -100,3 +102,65 @@ func TestNormalizeLinefeeds(t *testing.T) {
 		assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
 	}
 }
+
+func TestScanLinesAndTruncateWhenLongerThanBuffer(t *testing.T) {
+	type scenario struct {
+		input         string
+		expectedLines []string
+	}
+
+	scenarios := []scenario{
+		{
+			"",
+			[]string{},
+		},
+		{
+			"\n",
+			[]string{""},
+		},
+		{
+			"abc",
+			[]string{"abc"},
+		},
+		{
+			"abc\ndef",
+			[]string{"abc", "def"},
+		},
+		{
+			"abc\n\ndef",
+			[]string{"abc", "", "def"},
+		},
+		{
+			"abc\r\ndef\r",
+			[]string{"abc", "def"},
+		},
+		{
+			"abcdef",
+			[]string{"abcde"},
+		},
+		{
+			"abcdef\n",
+			[]string{"abcde"},
+		},
+		{
+			"abcdef\nghijkl\nx",
+			[]string{"abcde", "ghijk", "x"},
+		},
+		{
+			"abc\ndefghijklmnopqrstuvw\nx",
+			[]string{"abc", "defgh", "x"},
+		},
+	}
+
+	for _, s := range scenarios {
+		scanner := bufio.NewScanner(strings.NewReader(s.input))
+		scanner.Buffer(make([]byte, 5), 5)
+		scanner.Split(ScanLinesAndTruncateWhenLongerThanBuffer(5))
+		result := []string{}
+		for scanner.Scan() {
+			result = append(result, scanner.Text())
+		}
+		assert.NoError(t, scanner.Err())
+		assert.EqualValues(t, s.expectedLines, result)
+	}
+}