ronantakizawa/codereview-bench huggingface dataset:
Repository: jesseduffield/lazygit
File: pkg/gui/patch_building_panel.go (Go)

Code before:
package gui

import (
	"github.com/jesseduffield/lazygit/pkg/utils"
)

func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
	if !gui.git.Patch.PatchManager.Active() {
		return gui.handleEscapePatchBuildingPanel()
	}

	gui.Views.Main.Title = "Patch"
	gui.Views.Secondary.Title = "Custom Patch"

	// get diff from commit file that's currently selected
	node := gui.State.Contexts.CommitFiles.GetSelected()
	if node == nil {
		return nil
	}

	to := gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.GetRefName()
	parent := gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.GetParentRefName()
	from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(parent)
	diff, err := gui.git.WorkingTree.ShowFileDiff(from, to, reverse, node.GetPath(), true)
	if err != nil {
		return err
	}

	secondaryDiff := gui.git.Patch.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true)
	if err != nil {
		return err
	}

	empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx)
	if err != nil {
		return err
	}

	if empty {
		return gui.handleEscapePatchBuildingPanel()
	}

	return nil
}

func (gui *Gui) handleRefreshPatchBuildingPanel(selectedLineIdx int) error {
	gui.Mutexes.LineByLinePanelMutex.Lock()

Review comment (suggestion):
Looks like we always want to store the two refs at once, and to obtain the two refs at once. If so, could we have a get and set method that takes/returns both refs? E.g. GetRefNames() which returns the ref and parent ref.

Code after:
package gui

import "github.com/samber/lo"

func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
	if !gui.git.Patch.PatchManager.Active() {
		return gui.handleEscapePatchBuildingPanel()
	}

	gui.Views.Main.Title = "Patch"
	gui.Views.Secondary.Title = "Custom Patch"

	// get diff from commit file that's currently selected
	node := gui.State.Contexts.CommitFiles.GetSelected()
	if node == nil {
		return nil
	}

	ref := gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.GetRef()
	to := ref.RefName()
	from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
	diff, err := gui.git.WorkingTree.ShowFileDiff(from, to, reverse, node.GetPath(), true)
	if err != nil {
		return err
	}

	secondaryDiff := gui.git.Patch.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true)
	if err != nil {
		return err
	}

	empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx)
	if err != nil {
		return err
	}

	if empty {
		return gui.handleEscapePatchBuildingPanel()
	}

	return nil
}

func (gui *Gui) handleRefreshPatchBuildingPanel(selectedLineIdx int) error {
	gui.Mutexes.LineByLinePanelMutex.Lock()
	defer gui.Mutexes.LineByLinePanelMutex.Unlock()
Repository: jesseduffield/lazygit
File: pkg/utils/color.go (Go)

Code before:
package utils

import (
	"regexp"
)

// Decolorise strips a string of color
func Decolorise(str string) string {
	re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`)
	return re.ReplaceAllString(str, "")
}

func IsValidHexValue(v string) bool {
	if len(v) != 4 && len(v) != 7 {
		return false
	}

	if v[0] != '#' {
		return false
	}

	for _, char := range v[1:] {
		switch char {
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F':
			continue
		default:
			return false
		}
	}

	return true
}


Review comment (performance):
decolorise is quite expensive to run when you're trying to render a lot of stuff so we're caching it here. It would be nice if we delayed colouring things till the very end, so that we could do length-checks without needing to decolorise, but that would require some work

Code after:
package utils

import (
	"regexp"
	"sync"
)

var decoloriseCache = make(map[string]string)
var decoloriseMutex sync.Mutex

// Decolorise strips a string of color
func Decolorise(str string) string {
	decoloriseMutex.Lock()
	defer decoloriseMutex.Unlock()

	if decoloriseCache[str] != "" {
		return decoloriseCache[str]
	}

	re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`)
	ret := re.ReplaceAllString(str, "")

	decoloriseCache[str] = ret

	return ret
}

func IsValidHexValue(v string) bool {
	if len(v) != 4 && len(v) != 7 {
		return false
	}

	if v[0] != '#' {
Repository: jesseduffield/lazygit
File: pkg/gui/presentation/authors/authors.go (Go)

Code before:

	value := AuthorStyle(authorName).Sprint(initials)
	authorInitialCache[authorName] = value

	return value
}

func LongAuthor(authorName string) string {
	if value, ok := authorNameCache[authorName]; ok {
		return value
	}

	paddedAuthorName := utils.WithPadding(authorName, 17)
	truncatedName := utils.TruncateWithEllipsis(paddedAuthorName, 17)
	value := AuthorStyle(authorName).Sprint(truncatedName)
	authorNameCache[authorName] = value

	return value
}

func AuthorStyle(authorName string) style.TextStyle {
	if value, ok := authorStyleCache[authorName]; ok {
		return value
	}

	// use the unified style whatever the autor name is
	if value, ok := authorStyleCache[authorNameWildcard]; ok {
		return value
	}

	value := trueColorStyle(authorName)

	authorStyleCache[authorName] = value

	return value
}

func trueColorStyle(str string) style.TextStyle {
	hash := md5.Sum([]byte(str))
	c := colorful.Hsl(randFloat(hash[0:4])*360.0, 0.6+0.4*randFloat(hash[4:8]), 0.4+randFloat(hash[8:12])*0.2)

	return style.New().SetFg(style.NewRGBColor(color.RGB(uint8(c.R*255), uint8(c.G*255), uint8(c.B*255))))
}

func randFloat(hash []byte) float64 {
	return float64(randInt(hash, 100)) / 100
}

func randInt(hash []byte, max int) int {
	sum := 0
	for _, b := range hash {

Review comment (security):
```suggestion
	// use the unified style whatever the author name is
```

Code after:

	value := AuthorStyle(authorName).Sprint(initials)
	authorInitialCache[authorName] = value

	return value
}

func LongAuthor(authorName string) string {
	if value, ok := authorNameCache[authorName]; ok {
		return value
	}

	paddedAuthorName := utils.WithPadding(authorName, 17)
	truncatedName := utils.TruncateWithEllipsis(paddedAuthorName, 17)
	value := AuthorStyle(authorName).Sprint(truncatedName)
	authorNameCache[authorName] = value

	return value
}

func AuthorStyle(authorName string) style.TextStyle {
	if value, ok := authorStyleCache[authorName]; ok {
		return value
	}

	// use the unified style whatever the author name is
	if value, ok := authorStyleCache[authorNameWildcard]; ok {
		return value
	}

	value := trueColorStyle(authorName)

	authorStyleCache[authorName] = value

	return value
}

func trueColorStyle(str string) style.TextStyle {
	hash := md5.Sum([]byte(str))
	c := colorful.Hsl(randFloat(hash[0:4])*360.0, 0.6+0.4*randFloat(hash[4:8]), 0.4+randFloat(hash[8:12])*0.2)

	return style.New().SetFg(style.NewRGBColor(color.RGB(uint8(c.R*255), uint8(c.G*255), uint8(c.B*255))))
}

func randFloat(hash []byte) float64 {
	return float64(randInt(hash, 100)) / 100
}

func randInt(hash []byte, max int) int {
	sum := 0
	for _, b := range hash {
Repository: jesseduffield/lazygit
File: pkg/commands/oscommands/os.go (Go)

Code before:
}

func GetTempDir() string {
	return filepath.Join(os.TempDir(), "lazygit")
}

// GetLazygitPath returns the path of the currently executed file
func GetLazygitPath() string {
	ex, err := os.Executable() // get the executable path for git to use
	if err != nil {
		ex = os.Args[0] // fallback to the first call argument if needed
	}
	return `"` + filepath.ToSlash(ex) + `"`
}

func (c *OSCommand) UpdateWindowTitle() error {
	if runtime.GOOS != "windows" {
		return nil
	}
	path, getWdErr := os.Getwd()
	if getWdErr != nil {
		return getWdErr
	}
	title := fmt.Sprint(filepath.Base(path), " - Lazygit")
	args := append([]string{"cmd", "/C", "title"}, strings.Split(title, " ")...)
	return c.Cmd.NewShell(strings.Join(args, " ")).Run()
}


Review comment (suggestion):
if I check the output in the Command Log panel I see `bash -c "cmd /C title lazygit - Lazygit"`. I'm on osx so it uses bash but I suspect on windows it shows `cmd /C "cmd /C title lazygit - Lazygit`. In effect, the NewShell function adds the `cmd` and `/C` arguments itself. So I'd see how you go removing those two args here

Code after:

func GetTempDir() string {
	return filepath.Join(os.TempDir(), "lazygit")
}

// GetLazygitPath returns the path of the currently executed file
func GetLazygitPath() string {
	ex, err := os.Executable() // get the executable path for git to use
	if err != nil {
		ex = os.Args[0] // fallback to the first call argument if needed
	}
	return `"` + filepath.ToSlash(ex) + `"`
}

func (c *OSCommand) UpdateWindowTitle() error {
	if c.Platform.OS != "windows" {
		return nil
	}
	path, getWdErr := os.Getwd()
	if getWdErr != nil {
		return getWdErr
	}
	argString := fmt.Sprint("title ", filepath.Base(path), " - Lazygit")
	return c.Cmd.NewShell(argString).Run()
}
Repository: jesseduffield/lazygit
File: pkg/gui/gui.go (Go)

Code before:
	// so that you can return to the superproject
	RepoPathStack []string

	// this tells us whether our views have been initially set up
	ViewsSetup bool

	Views Views

	// if we've suspended the gui (e.g. because we've switched to a subprocess)
	// we typically want to pause some things that are running like background
	// file refreshes
	PauseBackgroundThreads bool

	// Log of the commands that get run, to be displayed to the user.
	CmdLog []string

	// the extras window contains things like the command log
	ShowExtrasWindow bool

	suggestionsAsyncHandler *tasks.AsyncHandler

	PopupHandler PopupHandler

	IsNewRepo bool

	InitialRepoDir string
}

type listPanelState struct {
	SelectedLineIdx int
}

func (h *listPanelState) SetSelectedLineIdx(value int) {
	h.SelectedLineIdx = value
}

func (h *listPanelState) GetSelectedLineIdx() int {
	return h.SelectedLineIdx
}

// for now the staging panel state, unlike the other panel states, is going to be
// non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future
type LblPanelState struct {
	*lbl.State
	SecondaryFocused bool // this is for if we show the left or right panel
}

type MergingPanelState struct {
	*mergeconflicts.State


Review comment (suggestion):
could we rename this to just `InitialDir`, just because it's not necessarily going to be the root directory of the repo

Code after:
	// so that you can return to the superproject
	RepoPathStack []string

	// this tells us whether our views have been initially set up
	ViewsSetup bool

	Views Views

	// if we've suspended the gui (e.g. because we've switched to a subprocess)
	// we typically want to pause some things that are running like background
	// file refreshes
	PauseBackgroundThreads bool

	// Log of the commands that get run, to be displayed to the user.
	CmdLog []string

	// the extras window contains things like the command log
	ShowExtrasWindow bool

	suggestionsAsyncHandler *tasks.AsyncHandler

	PopupHandler PopupHandler

	IsNewRepo bool

	// this is the initial dir we are in upon opening lazygit. We hold onto this
	// in case we want to restore it before quitting for users who have set up
	// the feature for changing directory upon quit.
	// The reason we don't just wait until quit time to handle changing directories
	// is because some users want to keep track of the current lazygit directory in an outside
	// process
	InitialDir string
}

type listPanelState struct {
	SelectedLineIdx int
}

func (h *listPanelState) SetSelectedLineIdx(value int) {
	h.SelectedLineIdx = value
}

func (h *listPanelState) GetSelectedLineIdx() int {
	return h.SelectedLineIdx
}

// for now the staging panel state, unlike the other panel states, is going to be
// non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future
type LblPanelState struct {
	*lbl.State
Repository: jesseduffield/lazygit
File: pkg/gui/options_menu_panel.go (Go)

Code before:
	}

	return binding.Description
}

func (gui *Gui) handleCreateOptionsMenu() error {
	context := gui.currentContext()
	bindings := gui.getBindings(context)

	menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem {
		return &types.MenuItem{
			Label: gui.displayDescription(binding),
			OnPress: func() error {
				if binding.Key == nil {
					return nil
				}

				return binding.Handler()
			},
			Key:     binding.Key,
			Tooltip: binding.Tooltip,
		}
	})

	return gui.c.Menu(types.CreateMenuOptions{
		Title:      cases.Title(language.Und).String(gui.c.Tr.LcMenu),
		Items:      menuItems,
		HideCancel: true,
	})
}


Review comment (suggestion):
rather than import the cases package, could we instead define a `gui.c.Tr.Menu` key which is manually titleized? Likewise with the other place we need to do this

Code after:
	return binding.Description
}

func (gui *Gui) handleCreateOptionsMenu() error {
	context := gui.currentContext()
	bindings := gui.getBindings(context)

	menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem {
		return &types.MenuItem{
			Label: gui.displayDescription(binding),
			OnPress: func() error {
				if binding.Key == nil {
					return nil
				}

				return binding.Handler()
			},
			Key:     binding.Key,
			Tooltip: binding.Tooltip,
		}
	})

	return gui.c.Menu(types.CreateMenuOptions{
		Title:      gui.c.Tr.MenuTitle,
		Items:      menuItems,
		HideCancel: true,
	})
}
Repository: jesseduffield/lazygit
File: pkg/commands/git_commands/stash.go (Go)

Code before:
		return err
	}

	if err := self.cmd.New("git stash drop stash@{1}").Run(); err != nil {
		return err
	}

	// if you had staged an untracked file, that will now appear as 'AD' in git status
	// meaning it's deleted in your working tree but added in your index. Given that it's
	// now safely stashed, we need to remove it.
	files := self.fileLoader.
		GetStatusFiles(loaders.GetStatusFileOptions{})

	for _, file := range files {
		if file.ShortStatus == "AD" {
			if err := self.workingTree.UnStageFile(file.Names(), false); err != nil {
				return err
			}
		}
	}

	return nil
}

func (self *StashCommands) StashUntrackedChanges(message string) error {
	if err := self.cmd.New("git stash -u").Run(); err != nil {
		return err
	}
	return nil
}



Review comment (suggestion):
Redundant `if`, you could just `return self.cmd.New()`.

I'd also suggest using the extended form of the flag, i.e. `--include-untracked`, just so it's easier to see what's going on.

Code after:
	// wrap in 'writing', which uses a mutex
	if err := self.cmd.New("git stash --keep-index").Run(); err != nil {
		return err
	}

	if err := self.Save(message); err != nil {
		return err
	}

	if err := self.cmd.New("git stash apply stash@{1}").Run(); err != nil {
		return err
	}

	if err := self.os.PipeCommands("git stash show -p", "git apply -R"); err != nil {
		return err
	}

	if err := self.cmd.New("git stash drop stash@{1}").Run(); err != nil {
		return err
	}

	// if you had staged an untracked file, that will now appear as 'AD' in git status
	// meaning it's deleted in your working tree but added in your index. Given that it's
	// now safely stashed, we need to remove it.
	files := self.fileLoader.
		GetStatusFiles(loaders.GetStatusFileOptions{})

	for _, file := range files {
		if file.ShortStatus == "AD" {
			if err := self.workingTree.UnStageFile(file.Names(), false); err != nil {
				return err
			}
		}
	}

	return nil
}

func (self *StashCommands) StashIncludeUntrackedChanges(message string) error {
	return self.cmd.New(fmt.Sprintf("git stash save %s --include-untracked", self.cmd.Quote(message))).Run()
}

func (self *StashCommands) Rename(index int, message string) error {
	sha, err := self.Sha(index)
	if err != nil {
		return err
	}

	if err := self.Drop(index); err != nil {
		return err
	}
Repository: jesseduffield/lazygit
File: pkg/integration/tests/stash/stash.go (Go)

Code before:
package stash

import (
	"github.com/jesseduffield/lazygit/pkg/config"
	. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var Stash = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Stashing files",
	ExtraCmdArgs: "",
	Skip:         false,
	SetupConfig:  func(config *config.AppConfig) {},
	SetupRepo: func(shell *Shell) {
		shell.EmptyCommit("initial commit")
		shell.CreateFile("file", "content")
		shell.GitAddAll()
	},
	Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
		assert.StashCount(0)

		input.PressKeys(keys.Files.ViewStashOptions)
		input.Confirm()
		input.Confirm()

		assert.StashCount(1)
	},
})


Review comment (suggestion):
you mentioned that there were no stashing examples in the existing tests. Although that's true, there are a few examples that can help with writing a test in general.

Because this test will press keys as fast as possible, it's up to you to have assertions after each key press to ensure the expected effect has occurred. So for example, when you go to view the stash options you can then do:
```go
		assert.InMenu()
		assert.MatchCurrentViewTitle(Equals("foo"))
		assert.MatchSelectedLine(Contains("bar"))
		
		input.Confirm()
		
		assert.InConfirm()
		assert.MatchCurrentViewTitle(Equals("Are you sure?"))
		input.Confirm()
```

Code after:
package stash

import (
	"github.com/jesseduffield/lazygit/pkg/config"
	. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var Stash = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Stashing files",
	ExtraCmdArgs: "",
	Skip:         false,
	SetupConfig:  func(config *config.AppConfig) {},
	SetupRepo: func(shell *Shell) {
		shell.EmptyCommit("initial commit")
		shell.CreateFile("file", "content")
		shell.GitAddAll()
	},
	Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
		assert.StashCount(0)
		assert.WorkingTreeFileCount(1)

		input.PressKeys(keys.Files.ViewStashOptions)
		assert.InMenu()

		input.PressKeys("a")
		assert.InPrompt()
		assert.MatchCurrentViewTitle(Equals("Stash changes"))

		input.Type("my stashed file")
		input.Confirm()
		assert.StashCount(1)
		assert.WorkingTreeFileCount(0)
	},
})
Repository: jesseduffield/lazygit
File: pkg/integration/tests/stash/stash.go (Go)

Code before:
package stash

import (
	"github.com/jesseduffield/lazygit/pkg/config"
	. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var Stash = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Stashing files",
	ExtraCmdArgs: "",
	Skip:         false,
	SetupConfig:  func(config *config.AppConfig) {},
	SetupRepo: func(shell *Shell) {
		shell.EmptyCommit("initial commit")
		shell.CreateFile("file", "content")
		shell.GitAddAll()
	},
	Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
		assert.StashCount(0)
		assert.WorkingTreeFileCount(1)

		input.PressKeys(keys.Files.ViewStashOptions)
		assert.InMenu()

		input.PressKeys("a")
		input.Type("stash name")
		input.Confirm()

		assert.StashCount(1)
		assert.WorkingTreeFileCount(0)
	},
})


Review comment (suggestion):
we should assert that when 'a' is pressed, a prompt appears. Here's an example from another file:
```go
input.PressKeys("a")
assert.InPrompt()
assert.MatchCurrentViewTitle(Equals("Enter a file name"))
input.Type("my file")
```

Code after:
package stash

import (
	"github.com/jesseduffield/lazygit/pkg/config"
	. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var Stash = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Stashing files",
	ExtraCmdArgs: "",
	Skip:         false,
	SetupConfig:  func(config *config.AppConfig) {},
	SetupRepo: func(shell *Shell) {
		shell.EmptyCommit("initial commit")
		shell.CreateFile("file", "content")
		shell.GitAddAll()
	},
	Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
		assert.StashCount(0)
		assert.WorkingTreeFileCount(1)

		input.PressKeys(keys.Files.ViewStashOptions)
		assert.InMenu()

		input.PressKeys("a")
		assert.InPrompt()
		assert.MatchCurrentViewTitle(Equals("Stash changes"))

		input.Type("my stashed file")
		input.Confirm()
		assert.StashCount(1)
		assert.WorkingTreeFileCount(0)
	},
})
Repository: jesseduffield/lazygit
File: pkg/commands/oscommands/os.go (Go)

Code before:

func (c *OSCommand) OpenLink(link string) error {
	commandTemplate := c.UserConfig.OS.OpenLinkCommand
	templateValues := map[string]string{
		"link": c.Quote(link),
	}

	command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
	return c.Cmd.NewShell(command).Run()
}

// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
	return c.Cmd.Quote(message)
}

// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
	c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o600)
	if err != nil {
		return utils.WrapError(err)
	}
	defer f.Close()

	info, err := os.Stat(filename)
	if err != nil {
		return utils.WrapError(err)
	}

	// read last char
	buf := make([]byte, 1)
	if _, err := f.ReadAt(buf, info.Size()-1); err != nil {
		return utils.WrapError(err)
	}

	// if the last byte is newline, don't add another
	if []byte("\n")[0] != buf[0] {
		_, err = f.WriteString("\n")
	}

	if err == nil {
		_, err = f.WriteString(line + "\n")
	}

	if err != nil {
		return utils.WrapError(err)
	}
	return nil
}


Review comment (suggestion):
Given that the logic here is a little complex, what are your thoughts on adding a test? I've found this testing package which lets you go and add files to an in-memory file system fairly easily: https://github.com/spf13/afero

We'd just have to swap out any place we use `os.<file-system method>` in this file with a call to `c.os.<file-system method>`, where by default the os field on our struct is just the default os, and from our test we can swap it out for our afero file system, similar to what we do with `getenvFn`.

Code after:

func (c *OSCommand) OpenLink(link string) error {
	commandTemplate := c.UserConfig.OS.OpenLinkCommand
	templateValues := map[string]string{
		"link": c.Quote(link),
	}

	command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
	return c.Cmd.NewShell(command).Run()
}

// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
	return c.Cmd.Quote(message)
}

// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
	c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o600)
	if err != nil {
		return utils.WrapError(err)
	}
	defer f.Close()

	info, err := os.Stat(filename)
	if err != nil {
		return utils.WrapError(err)
	}

	if info.Size() > 0 {
		// read last char
		buf := make([]byte, 1)
		if _, err := f.ReadAt(buf, info.Size()-1); err != nil {
			return utils.WrapError(err)
		}

		// if the last byte of the file is not a newline, add it
		if []byte("\n")[0] != buf[0] {
			_, err = f.WriteString("\n")
		}
	}

	if err == nil {
		_, err = f.WriteString(line + "\n")
	}

	if err != nil {
		return utils.WrapError(err)
	}
	return nil