From 4e8e4612bd20ed12fa4b64f825f050e45ce5f9bd Mon Sep 17 00:00:00 2001 From: Denis Isaev Date: Sat, 24 Nov 2018 12:15:54 +0300 Subject: [PATCH 1/8] fix 'main' redefinition in scripts/ dir --- scripts/generate_cheatsheet.go | 54 ------------------- .../main.go} | 2 +- 2 files changed, 1 insertion(+), 55 deletions(-) delete mode 100644 scripts/generate_cheatsheet.go rename scripts/{push_new_patch.go => push_new_patch/main.go} (97%) diff --git a/scripts/generate_cheatsheet.go b/scripts/generate_cheatsheet.go deleted file mode 100644 index cfa6d92ad..000000000 --- a/scripts/generate_cheatsheet.go +++ /dev/null @@ -1,54 +0,0 @@ -// run: -// LANG=en go run generate_cheatsheet.go -// to generate Keybindings_en.md file in current directory -// change LANG to generate cheatsheet in different language (if supported) - -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/jesseduffield/lazygit/pkg/app" - "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func main() { - appConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool)) - a, _ := app.NewApp(appConfig) - lang := a.Tr.GetLanguage() - name := "Keybindings_" + lang + ".md" - bindings := a.Gui.GetKeybindings() - padWidth := a.Gui.GetMaxKeyLength(bindings) - file, _ := os.Create(name) - current := "v" - content := "" - title := "" - - file.WriteString("# Lazygit " + a.Tr.SLocalize("menu")) - - for _, binding := range bindings { - if key := a.Gui.GetKey(binding); key != "" && (binding.Description != "" || key == "x") { - if binding.ViewName != current { - current = binding.ViewName - if current == "" { - title = a.Tr.SLocalize("GlobalTitle") - } else { - title = a.Tr.SLocalize(strings.Title(current) + "Title") - } - content = fmt.Sprintf("\n\n## %s\n
\n", title)
-				file.WriteString(content)
-			}
-			// workaround to include menu keybinding in cheatsheet
-			// could not add this Description field directly to keybindings.go,
-			// because then menu key would be displayed in menu itself and that is undesirable
-			if key == "x" {
-				binding.Description = a.Tr.SLocalize("menu")
-			}
-			content = fmt.Sprintf("\t%s%s  %s\n", key, strings.TrimPrefix(utils.WithPadding(key, padWidth), key), binding.Description)
-			file.WriteString(content)
-		}
-	}
-}
diff --git a/scripts/push_new_patch.go b/scripts/push_new_patch/main.go
similarity index 97%
rename from scripts/push_new_patch.go
rename to scripts/push_new_patch/main.go
index e11865fff..b1660d3c1 100755
--- a/scripts/push_new_patch.go
+++ b/scripts/push_new_patch/main.go
@@ -1,5 +1,5 @@
 // call from project root with
-// go run bin/push_new_patch.go
+// go run scripts/push_new_patch/main.go
 
 // goreleaser expects a $GITHUB_TOKEN env variable to be defined
 // in order to push the release got github

From 5c70d2724b508c3021558162fbfd9792c2d77e63 Mon Sep 17 00:00:00 2001
From: BlakeMScurr 
Date: Wed, 28 Nov 2018 12:33:52 +1300
Subject: [PATCH 2/8] Fix function comments with CodeLingo.

---
 pkg/commands/os.go      | 2 +-
 pkg/commands/os_test.go | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pkg/commands/os.go b/pkg/commands/os.go
index 2caedf07d..8b4b7879e 100644
--- a/pkg/commands/os.go
+++ b/pkg/commands/os.go
@@ -109,7 +109,7 @@ func (c *OSCommand) OpenFile(filename string) error {
 	return err
 }
 
-// OpenFile opens a file with the given
+// OpenLink opens a file with the given
 func (c *OSCommand) OpenLink(link string) error {
 	commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
 	templateValues := map[string]string{
diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go
index aeef4a6e5..9a238c1a3 100644
--- a/pkg/commands/os_test.go
+++ b/pkg/commands/os_test.go
@@ -278,7 +278,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
 	assert.EqualValues(t, expected, actual)
 }
 
-// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux
+// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
 func TestOSCommandQuoteDoubleQuote(t *testing.T) {
 	osCommand := newDummyOSCommand()
 

From 643cdd3461007fafb1fd9284a65b54f36e55ddac Mon Sep 17 00:00:00 2001
From: BlakeMScurr 
Date: Fri, 30 Nov 2018 11:04:08 +1300
Subject: [PATCH 3/8] Add simple comments to uncommented functions.

---
 pkg/commands/commit.go            |  1 +
 pkg/commands/git.go               |  1 +
 pkg/commands/git_test.go          | 43 +++++++++++++++++++++++++++++++
 pkg/commands/os_test.go           |  7 +++++
 pkg/commands/pull_request_test.go |  2 ++
 pkg/gui/commit_message_panel.go   |  1 +
 pkg/gui/keybindings.go            |  2 ++
 pkg/gui/recent_repos_panel.go     |  1 +
 pkg/i18n/i18n_test.go             |  3 +++
 pkg/utils/utils_test.go           | 14 ++++++++++
 10 files changed, 75 insertions(+)

diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go
index 54e94ef60..908677a18 100644
--- a/pkg/commands/commit.go
+++ b/pkg/commands/commit.go
@@ -13,6 +13,7 @@ type Commit struct {
 	DisplayString string
 }
 
+// GetDisplayStrings is a function
 func (c *Commit) GetDisplayStrings() []string {
 	red := color.New(color.FgRed)
 	yellow := color.New(color.FgGreen)
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index a92ce3435..a5add569a 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -262,6 +262,7 @@ func (c *GitCommand) NewBranch(name string) error {
 	return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
 }
 
+// CurrentBranchName is a function
 func (c *GitCommand) CurrentBranchName() (string, error) {
 	branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
 	if err != nil {
diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go
index 9bfdaf519..5924ba226 100644
--- a/pkg/commands/git_test.go
+++ b/pkg/commands/git_test.go
@@ -23,26 +23,32 @@ type fileInfoMock struct {
 	sys         interface{}
 }
 
+// Name is a function
 func (f fileInfoMock) Name() string {
 	return f.name
 }
 
+// Size is a function
 func (f fileInfoMock) Size() int64 {
 	return f.size
 }
 
+// Mode is a function
 func (f fileInfoMock) Mode() os.FileMode {
 	return f.fileMode
 }
 
+// ModTime is a function
 func (f fileInfoMock) ModTime() time.Time {
 	return f.fileModTime
 }
 
+// IsDir is a function
 func (f fileInfoMock) IsDir() bool {
 	return f.isDir
 }
 
+// Sys is a function
 func (f fileInfoMock) Sys() interface{} {
 	return f.sys
 }
@@ -64,6 +70,7 @@ func newDummyGitCommand() *GitCommand {
 	}
 }
 
+// TestVerifyInGitRepo is a function
 func TestVerifyInGitRepo(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -100,6 +107,7 @@ func TestVerifyInGitRepo(t *testing.T) {
 	}
 }
 
+// TestNavigateToRepoRootDirectory is a function
 func TestNavigateToRepoRootDirectory(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -156,6 +164,7 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
 	}
 }
 
+// TestSetupRepositoryAndWorktree is a function
 func TestSetupRepositoryAndWorktree(t *testing.T) {
 	type scenario struct {
 		testName          string
@@ -224,6 +233,7 @@ func TestSetupRepositoryAndWorktree(t *testing.T) {
 	}
 }
 
+// TestNewGitCommand is a function
 func TestNewGitCommand(t *testing.T) {
 	actual, err := os.Getwd()
 	assert.NoError(t, err)
@@ -271,6 +281,7 @@ func TestNewGitCommand(t *testing.T) {
 	}
 }
 
+// TestGitCommandGetStashEntries is a function
 func TestGitCommandGetStashEntries(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -323,6 +334,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
 	}
 }
 
+// TestGitCommandGetStashEntryDiff is a function
 func TestGitCommandGetStashEntryDiff(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -337,6 +349,7 @@ func TestGitCommandGetStashEntryDiff(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+// TestGitCommandGetStatusFiles is a function
 func TestGitCommandGetStatusFiles(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -423,6 +436,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
 	}
 }
 
+// TestGitCommandStashDo is a function
 func TestGitCommandStashDo(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -435,6 +449,7 @@ func TestGitCommandStashDo(t *testing.T) {
 	assert.NoError(t, gitCmd.StashDo(1, "drop"))
 }
 
+// TestGitCommandStashSave is a function
 func TestGitCommandStashSave(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -447,6 +462,7 @@ func TestGitCommandStashSave(t *testing.T) {
 	assert.NoError(t, gitCmd.StashSave("A stash message"))
 }
 
+// TestGitCommandCommitAmend is a function
 func TestGitCommandCommitAmend(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -460,6 +476,7 @@ func TestGitCommandCommitAmend(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+// TestGitCommandMergeStatusFiles is a function
 func TestGitCommandMergeStatusFiles(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -540,6 +557,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
 	}
 }
 
+// TestGitCommandUpstreamDifferentCount is a function
 func TestGitCommandUpstreamDifferentCount(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -597,6 +615,7 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) {
 	}
 }
 
+// TestGitCommandGetCommitsToPush is a function
 func TestGitCommandGetCommitsToPush(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -635,6 +654,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) {
 	}
 }
 
+// TestGitCommandRenameCommit is a function
 func TestGitCommandRenameCommit(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -647,6 +667,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
 	assert.NoError(t, gitCmd.RenameCommit("test"))
 }
 
+// TestGitCommandResetToCommit is a function
 func TestGitCommandResetToCommit(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -659,6 +680,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
 	assert.NoError(t, gitCmd.ResetToCommit("78976bc"))
 }
 
+// TestGitCommandNewBranch is a function
 func TestGitCommandNewBranch(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -671,6 +693,7 @@ func TestGitCommandNewBranch(t *testing.T) {
 	assert.NoError(t, gitCmd.NewBranch("test"))
 }
 
+// TestGitCommandDeleteBranch is a function
 func TestGitCommandDeleteBranch(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -720,6 +743,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
 	}
 }
 
+// TestGitCommandMerge is a function
 func TestGitCommandMerge(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -732,6 +756,7 @@ func TestGitCommandMerge(t *testing.T) {
 	assert.NoError(t, gitCmd.Merge("test"))
 }
 
+// TestGitCommandUsingGpg is a function
 func TestGitCommandUsingGpg(t *testing.T) {
 	type scenario struct {
 		testName           string
@@ -825,6 +850,7 @@ func TestGitCommandUsingGpg(t *testing.T) {
 	}
 }
 
+// TestGitCommandCommit is a function
 func TestGitCommandCommit(t *testing.T) {
 	type scenario struct {
 		testName           string
@@ -894,6 +920,7 @@ func TestGitCommandCommit(t *testing.T) {
 	}
 }
 
+// TestGitCommandCommitAmendFromFiles is a function
 func TestGitCommandCommitAmendFromFiles(t *testing.T) {
 	type scenario struct {
 		testName           string
@@ -963,6 +990,7 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) {
 	}
 }
 
+// TestGitCommandPush is a function
 func TestGitCommandPush(t *testing.T) {
 	type scenario struct {
 		testName  string
@@ -1022,6 +1050,7 @@ func TestGitCommandPush(t *testing.T) {
 	}
 }
 
+// TestGitCommandSquashPreviousTwoCommits is a function
 func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1085,6 +1114,7 @@ func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
 	}
 }
 
+// TestGitCommandSquashFixupCommit is a function
 func TestGitCommandSquashFixupCommit(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1148,6 +1178,7 @@ func TestGitCommandSquashFixupCommit(t *testing.T) {
 	}
 }
 
+// TestGitCommandCatFile is a function
 func TestGitCommandCatFile(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1162,6 +1193,7 @@ func TestGitCommandCatFile(t *testing.T) {
 	assert.Equal(t, "test", o)
 }
 
+// TestGitCommandStageFile is a function
 func TestGitCommandStageFile(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1174,6 +1206,7 @@ func TestGitCommandStageFile(t *testing.T) {
 	assert.NoError(t, gitCmd.StageFile("test.txt"))
 }
 
+// TestGitCommandUnstageFile is a function
 func TestGitCommandUnstageFile(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1220,6 +1253,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
 	}
 }
 
+// TestGitCommandIsInMergeState is a function
 func TestGitCommandIsInMergeState(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1288,6 +1322,7 @@ func TestGitCommandIsInMergeState(t *testing.T) {
 	}
 }
 
+// TestGitCommandRemoveFile is a function
 func TestGitCommandRemoveFile(t *testing.T) {
 	type scenario struct {
 		testName   string
@@ -1489,6 +1524,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 	}
 }
 
+// TestGitCommandShow is a function
 func TestGitCommandShow(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1502,6 +1538,7 @@ func TestGitCommandShow(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+// TestGitCommandCheckout is a function
 func TestGitCommandCheckout(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1548,6 +1585,7 @@ func TestGitCommandCheckout(t *testing.T) {
 	}
 }
 
+// TestGitCommandGetBranchGraph is a function
 func TestGitCommandGetBranchGraph(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1561,6 +1599,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+// TestGitCommandGetCommits is a function
 func TestGitCommandGetCommits(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1682,6 +1721,7 @@ func TestGitCommandGetCommits(t *testing.T) {
 	}
 }
 
+// TestGitCommandGetLog is a function
 func TestGitCommandGetLog(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1724,6 +1764,7 @@ func TestGitCommandGetLog(t *testing.T) {
 	}
 }
 
+// TestGitCommandDiff is a function
 func TestGitCommandDiff(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1786,6 +1827,7 @@ func TestGitCommandDiff(t *testing.T) {
 	}
 }
 
+// TestGitCommandGetMergeBase is a function
 func TestGitCommandGetMergeBase(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1875,6 +1917,7 @@ func TestGitCommandGetMergeBase(t *testing.T) {
 	}
 }
 
+// TestGitCommandCurrentBranchName is a function
 func TestGitCommandCurrentBranchName(t *testing.T) {
 	type scenario struct {
 		testName string
diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go
index 9a238c1a3..82264ecef 100644
--- a/pkg/commands/os_test.go
+++ b/pkg/commands/os_test.go
@@ -29,6 +29,7 @@ func newDummyAppConfig() *config.AppConfig {
 	return appConfig
 }
 
+// TestOSCommandRunCommandWithOutput is a function
 func TestOSCommandRunCommandWithOutput(t *testing.T) {
 	type scenario struct {
 		command string
@@ -56,6 +57,7 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
 	}
 }
 
+// TestOSCommandRunCommand is a function
 func TestOSCommandRunCommand(t *testing.T) {
 	type scenario struct {
 		command string
@@ -76,6 +78,7 @@ func TestOSCommandRunCommand(t *testing.T) {
 	}
 }
 
+// TestOSCommandOpenFile is a function
 func TestOSCommandOpenFile(t *testing.T) {
 	type scenario struct {
 		filename string
@@ -126,6 +129,7 @@ func TestOSCommandOpenFile(t *testing.T) {
 	}
 }
 
+// TestOSCommandEditFile is a function
 func TestOSCommandEditFile(t *testing.T) {
 	type scenario struct {
 		filename           string
@@ -255,6 +259,7 @@ func TestOSCommandEditFile(t *testing.T) {
 	}
 }
 
+// TestOSCommandQuote is a function
 func TestOSCommandQuote(t *testing.T) {
 	osCommand := newDummyOSCommand()
 
@@ -291,6 +296,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
 	assert.EqualValues(t, expected, actual)
 }
 
+// TestOSCommandUnquote is a function
 func TestOSCommandUnquote(t *testing.T) {
 	osCommand := newDummyOSCommand()
 
@@ -301,6 +307,7 @@ func TestOSCommandUnquote(t *testing.T) {
 	assert.EqualValues(t, expected, actual)
 }
 
+// TestOSCommandFileType is a function
 func TestOSCommandFileType(t *testing.T) {
 	type scenario struct {
 		path  string
diff --git a/pkg/commands/pull_request_test.go b/pkg/commands/pull_request_test.go
index a551ee081..a8d0ac608 100644
--- a/pkg/commands/pull_request_test.go
+++ b/pkg/commands/pull_request_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+// TestGetRepoInfoFromURL is a function
 func TestGetRepoInfoFromURL(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -41,6 +42,7 @@ func TestGetRepoInfoFromURL(t *testing.T) {
 	}
 }
 
+// TestCreatePullRequest is a function
 func TestCreatePullRequest(t *testing.T) {
 	type scenario struct {
 		testName string
diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go
index c26b5573a..6f828312c 100644
--- a/pkg/gui/commit_message_panel.go
+++ b/pkg/gui/commit_message_panel.go
@@ -82,6 +82,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
 	return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
 }
 
+// RenderCommitLength is a function
 func (gui *Gui) RenderCommitLength() {
 	if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
 		return
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 8d07429a6..6c1f6c296 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -21,6 +21,7 @@ func (b *Binding) GetDisplayStrings() []string {
 	return []string{b.GetKey(), b.Description}
 }
 
+// GetKey is a function
 func (b *Binding) GetKey() string {
 	r, ok := b.Key.(rune)
 	key := ""
@@ -34,6 +35,7 @@ func (b *Binding) GetKey() string {
 	return key
 }
 
+// GetKeybindings is a function
 func (gui *Gui) GetKeybindings() []*Binding {
 	bindings := []*Binding{
 		{
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index add987b32..a146166bf 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -14,6 +14,7 @@ type recentRepo struct {
 	path string
 }
 
+// GetDisplayStrings is a function
 func (r *recentRepo) GetDisplayStrings() []string {
 	yellow := color.New(color.FgMagenta)
 	base := filepath.Base(r.path)
diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go
index 5ddfdd79c..7ad5670d9 100644
--- a/pkg/i18n/i18n_test.go
+++ b/pkg/i18n/i18n_test.go
@@ -16,10 +16,12 @@ func getDummyLog() *logrus.Entry {
 	log.Out = ioutil.Discard
 	return log.WithField("test", "test")
 }
+// TestNewLocalizer is a function
 func TestNewLocalizer(t *testing.T) {
 	assert.NotNil(t, NewLocalizer(getDummyLog()))
 }
 
+// TestDetectLanguage is a function
 func TestDetectLanguage(t *testing.T) {
 	type scenario struct {
 		langDetector func() (string, error)
@@ -46,6 +48,7 @@ func TestDetectLanguage(t *testing.T) {
 	}
 }
 
+// TestLocalizer is a function
 func TestLocalizer(t *testing.T) {
 	type scenario struct {
 		userLang string
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index 21e1d5031..de86eaef8 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+// TestSplitLines is a function
 func TestSplitLines(t *testing.T) {
 	type scenario struct {
 		multilineString string
@@ -36,6 +37,7 @@ func TestSplitLines(t *testing.T) {
 	}
 }
 
+// TestWithPadding is a function
 func TestWithPadding(t *testing.T) {
 	type scenario struct {
 		str      string
@@ -61,6 +63,7 @@ func TestWithPadding(t *testing.T) {
 	}
 }
 
+// TestTrimTrailingNewline is a function
 func TestTrimTrailingNewline(t *testing.T) {
 	type scenario struct {
 		str      string
@@ -83,6 +86,7 @@ func TestTrimTrailingNewline(t *testing.T) {
 	}
 }
 
+// TestNormalizeLinefeeds is a function
 func TestNormalizeLinefeeds(t *testing.T) {
 	type scenario struct {
 		byteArray []byte
@@ -116,6 +120,7 @@ func TestNormalizeLinefeeds(t *testing.T) {
 	}
 }
 
+// TestResolvePlaceholderString is a function
 func TestResolvePlaceholderString(t *testing.T) {
 	type scenario struct {
 		templateString string
@@ -169,6 +174,7 @@ func TestResolvePlaceholderString(t *testing.T) {
 	}
 }
 
+// TestDisplayArraysAligned is a function
 func TestDisplayArraysAligned(t *testing.T) {
 	type scenario struct {
 		input    [][]string
@@ -197,10 +203,12 @@ type myDisplayable struct {
 
 type myStruct struct{}
 
+// GetDisplayStrings is a function
 func (d *myDisplayable) GetDisplayStrings() []string {
 	return d.strings
 }
 
+// TestGetDisplayStringArrays is a function
 func TestGetDisplayStringArrays(t *testing.T) {
 	type scenario struct {
 		input    []Displayable
@@ -222,6 +230,7 @@ func TestGetDisplayStringArrays(t *testing.T) {
 	}
 }
 
+// TestRenderDisplayableList is a function
 func TestRenderDisplayableList(t *testing.T) {
 	type scenario struct {
 		input          []Displayable
@@ -263,6 +272,7 @@ func TestRenderDisplayableList(t *testing.T) {
 	}
 }
 
+// TestRenderList is a function
 func TestRenderList(t *testing.T) {
 	type scenario struct {
 		input          interface{}
@@ -301,6 +311,7 @@ func TestRenderList(t *testing.T) {
 	}
 }
 
+// TestGetPaddedDisplayStrings is a function
 func TestGetPaddedDisplayStrings(t *testing.T) {
 	type scenario struct {
 		stringArrays [][]string
@@ -321,6 +332,7 @@ func TestGetPaddedDisplayStrings(t *testing.T) {
 	}
 }
 
+// TestGetPadWidths is a function
 func TestGetPadWidths(t *testing.T) {
 	type scenario struct {
 		stringArrays [][]string
@@ -347,6 +359,7 @@ func TestGetPadWidths(t *testing.T) {
 	}
 }
 
+// TestMin is a function
 func TestMin(t *testing.T) {
 	type scenario struct {
 		a        int
@@ -377,6 +390,7 @@ func TestMin(t *testing.T) {
 	}
 }
 
+// TestIncludesString is a function
 func TestIncludesString(t *testing.T) {
 	type scenario struct {
 		list     []string

From 181f91d2efa816bddfb296971372c1cb675a370e Mon Sep 17 00:00:00 2001
From: BlakeMScurr 
Date: Fri, 30 Nov 2018 13:47:14 +1300
Subject: [PATCH 4/8] Add full stops to new comments.

---
 pkg/commands/commit.go            |  2 +-
 pkg/commands/git.go               |  2 +-
 pkg/commands/git_test.go          | 86 +++++++++++++++----------------
 pkg/commands/os_test.go           | 14 ++---
 pkg/commands/pull_request_test.go |  4 +-
 pkg/gui/commit_message_panel.go   |  2 +-
 pkg/gui/keybindings.go            |  4 +-
 pkg/gui/recent_repos_panel.go     |  2 +-
 pkg/i18n/i18n_test.go             |  7 +--
 pkg/utils/utils_test.go           | 28 +++++-----
 10 files changed, 76 insertions(+), 75 deletions(-)

diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go
index 908677a18..4c304271d 100644
--- a/pkg/commands/commit.go
+++ b/pkg/commands/commit.go
@@ -13,7 +13,7 @@ type Commit struct {
 	DisplayString string
 }
 
-// GetDisplayStrings is a function
+// GetDisplayStrings is a function.
 func (c *Commit) GetDisplayStrings() []string {
 	red := color.New(color.FgRed)
 	yellow := color.New(color.FgGreen)
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index a5add569a..adc26b8c8 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -262,7 +262,7 @@ func (c *GitCommand) NewBranch(name string) error {
 	return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
 }
 
-// CurrentBranchName is a function
+// CurrentBranchName is a function.
 func (c *GitCommand) CurrentBranchName() (string, error) {
 	branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
 	if err != nil {
diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go
index 5924ba226..aec928ea5 100644
--- a/pkg/commands/git_test.go
+++ b/pkg/commands/git_test.go
@@ -23,32 +23,32 @@ type fileInfoMock struct {
 	sys         interface{}
 }
 
-// Name is a function
+// Name is a function.
 func (f fileInfoMock) Name() string {
 	return f.name
 }
 
-// Size is a function
+// Size is a function.
 func (f fileInfoMock) Size() int64 {
 	return f.size
 }
 
-// Mode is a function
+// Mode is a function.
 func (f fileInfoMock) Mode() os.FileMode {
 	return f.fileMode
 }
 
-// ModTime is a function
+// ModTime is a function.
 func (f fileInfoMock) ModTime() time.Time {
 	return f.fileModTime
 }
 
-// IsDir is a function
+// IsDir is a function.
 func (f fileInfoMock) IsDir() bool {
 	return f.isDir
 }
 
-// Sys is a function
+// Sys is a function.
 func (f fileInfoMock) Sys() interface{} {
 	return f.sys
 }
@@ -70,7 +70,7 @@ func newDummyGitCommand() *GitCommand {
 	}
 }
 
-// TestVerifyInGitRepo is a function
+// TestVerifyInGitRepo is a function.
 func TestVerifyInGitRepo(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -107,7 +107,7 @@ func TestVerifyInGitRepo(t *testing.T) {
 	}
 }
 
-// TestNavigateToRepoRootDirectory is a function
+// TestNavigateToRepoRootDirectory is a function.
 func TestNavigateToRepoRootDirectory(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -164,7 +164,7 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
 	}
 }
 
-// TestSetupRepositoryAndWorktree is a function
+// TestSetupRepositoryAndWorktree is a function.
 func TestSetupRepositoryAndWorktree(t *testing.T) {
 	type scenario struct {
 		testName          string
@@ -233,7 +233,7 @@ func TestSetupRepositoryAndWorktree(t *testing.T) {
 	}
 }
 
-// TestNewGitCommand is a function
+// TestNewGitCommand is a function.
 func TestNewGitCommand(t *testing.T) {
 	actual, err := os.Getwd()
 	assert.NoError(t, err)
@@ -281,7 +281,7 @@ func TestNewGitCommand(t *testing.T) {
 	}
 }
 
-// TestGitCommandGetStashEntries is a function
+// TestGitCommandGetStashEntries is a function.
 func TestGitCommandGetStashEntries(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -334,7 +334,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
 	}
 }
 
-// TestGitCommandGetStashEntryDiff is a function
+// TestGitCommandGetStashEntryDiff is a function.
 func TestGitCommandGetStashEntryDiff(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -349,7 +349,7 @@ func TestGitCommandGetStashEntryDiff(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TestGitCommandGetStatusFiles is a function
+// TestGitCommandGetStatusFiles is a function.
 func TestGitCommandGetStatusFiles(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -436,7 +436,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
 	}
 }
 
-// TestGitCommandStashDo is a function
+// TestGitCommandStashDo is a function.
 func TestGitCommandStashDo(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -449,7 +449,7 @@ func TestGitCommandStashDo(t *testing.T) {
 	assert.NoError(t, gitCmd.StashDo(1, "drop"))
 }
 
-// TestGitCommandStashSave is a function
+// TestGitCommandStashSave is a function.
 func TestGitCommandStashSave(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -462,7 +462,7 @@ func TestGitCommandStashSave(t *testing.T) {
 	assert.NoError(t, gitCmd.StashSave("A stash message"))
 }
 
-// TestGitCommandCommitAmend is a function
+// TestGitCommandCommitAmend is a function.
 func TestGitCommandCommitAmend(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -476,7 +476,7 @@ func TestGitCommandCommitAmend(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TestGitCommandMergeStatusFiles is a function
+// TestGitCommandMergeStatusFiles is a function.
 func TestGitCommandMergeStatusFiles(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -557,7 +557,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
 	}
 }
 
-// TestGitCommandUpstreamDifferentCount is a function
+// TestGitCommandUpstreamDifferentCount is a function.
 func TestGitCommandUpstreamDifferentCount(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -615,7 +615,7 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) {
 	}
 }
 
-// TestGitCommandGetCommitsToPush is a function
+// TestGitCommandGetCommitsToPush is a function.
 func TestGitCommandGetCommitsToPush(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -654,7 +654,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) {
 	}
 }
 
-// TestGitCommandRenameCommit is a function
+// TestGitCommandRenameCommit is a function.
 func TestGitCommandRenameCommit(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -667,7 +667,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
 	assert.NoError(t, gitCmd.RenameCommit("test"))
 }
 
-// TestGitCommandResetToCommit is a function
+// TestGitCommandResetToCommit is a function.
 func TestGitCommandResetToCommit(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -680,7 +680,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
 	assert.NoError(t, gitCmd.ResetToCommit("78976bc"))
 }
 
-// TestGitCommandNewBranch is a function
+// TestGitCommandNewBranch is a function.
 func TestGitCommandNewBranch(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -693,7 +693,7 @@ func TestGitCommandNewBranch(t *testing.T) {
 	assert.NoError(t, gitCmd.NewBranch("test"))
 }
 
-// TestGitCommandDeleteBranch is a function
+// TestGitCommandDeleteBranch is a function.
 func TestGitCommandDeleteBranch(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -743,7 +743,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
 	}
 }
 
-// TestGitCommandMerge is a function
+// TestGitCommandMerge is a function.
 func TestGitCommandMerge(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -756,7 +756,7 @@ func TestGitCommandMerge(t *testing.T) {
 	assert.NoError(t, gitCmd.Merge("test"))
 }
 
-// TestGitCommandUsingGpg is a function
+// TestGitCommandUsingGpg is a function.
 func TestGitCommandUsingGpg(t *testing.T) {
 	type scenario struct {
 		testName           string
@@ -850,7 +850,7 @@ func TestGitCommandUsingGpg(t *testing.T) {
 	}
 }
 
-// TestGitCommandCommit is a function
+// TestGitCommandCommit is a function.
 func TestGitCommandCommit(t *testing.T) {
 	type scenario struct {
 		testName           string
@@ -920,7 +920,7 @@ func TestGitCommandCommit(t *testing.T) {
 	}
 }
 
-// TestGitCommandCommitAmendFromFiles is a function
+// TestGitCommandCommitAmendFromFiles is a function.
 func TestGitCommandCommitAmendFromFiles(t *testing.T) {
 	type scenario struct {
 		testName           string
@@ -990,7 +990,7 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) {
 	}
 }
 
-// TestGitCommandPush is a function
+// TestGitCommandPush is a function.
 func TestGitCommandPush(t *testing.T) {
 	type scenario struct {
 		testName  string
@@ -1050,7 +1050,7 @@ func TestGitCommandPush(t *testing.T) {
 	}
 }
 
-// TestGitCommandSquashPreviousTwoCommits is a function
+// TestGitCommandSquashPreviousTwoCommits is a function.
 func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1114,7 +1114,7 @@ func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
 	}
 }
 
-// TestGitCommandSquashFixupCommit is a function
+// TestGitCommandSquashFixupCommit is a function.
 func TestGitCommandSquashFixupCommit(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1178,7 +1178,7 @@ func TestGitCommandSquashFixupCommit(t *testing.T) {
 	}
 }
 
-// TestGitCommandCatFile is a function
+// TestGitCommandCatFile is a function.
 func TestGitCommandCatFile(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1193,7 +1193,7 @@ func TestGitCommandCatFile(t *testing.T) {
 	assert.Equal(t, "test", o)
 }
 
-// TestGitCommandStageFile is a function
+// TestGitCommandStageFile is a function.
 func TestGitCommandStageFile(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1206,7 +1206,7 @@ func TestGitCommandStageFile(t *testing.T) {
 	assert.NoError(t, gitCmd.StageFile("test.txt"))
 }
 
-// TestGitCommandUnstageFile is a function
+// TestGitCommandUnstageFile is a function.
 func TestGitCommandUnstageFile(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1253,7 +1253,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
 	}
 }
 
-// TestGitCommandIsInMergeState is a function
+// TestGitCommandIsInMergeState is a function.
 func TestGitCommandIsInMergeState(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1322,7 +1322,7 @@ func TestGitCommandIsInMergeState(t *testing.T) {
 	}
 }
 
-// TestGitCommandRemoveFile is a function
+// TestGitCommandRemoveFile is a function.
 func TestGitCommandRemoveFile(t *testing.T) {
 	type scenario struct {
 		testName   string
@@ -1524,7 +1524,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
 	}
 }
 
-// TestGitCommandShow is a function
+// TestGitCommandShow is a function.
 func TestGitCommandShow(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1538,7 +1538,7 @@ func TestGitCommandShow(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TestGitCommandCheckout is a function
+// TestGitCommandCheckout is a function.
 func TestGitCommandCheckout(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1585,7 +1585,7 @@ func TestGitCommandCheckout(t *testing.T) {
 	}
 }
 
-// TestGitCommandGetBranchGraph is a function
+// TestGitCommandGetBranchGraph is a function.
 func TestGitCommandGetBranchGraph(t *testing.T) {
 	gitCmd := newDummyGitCommand()
 	gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1599,7 +1599,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-// TestGitCommandGetCommits is a function
+// TestGitCommandGetCommits is a function.
 func TestGitCommandGetCommits(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1721,7 +1721,7 @@ func TestGitCommandGetCommits(t *testing.T) {
 	}
 }
 
-// TestGitCommandGetLog is a function
+// TestGitCommandGetLog is a function.
 func TestGitCommandGetLog(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1764,7 +1764,7 @@ func TestGitCommandGetLog(t *testing.T) {
 	}
 }
 
-// TestGitCommandDiff is a function
+// TestGitCommandDiff is a function.
 func TestGitCommandDiff(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1827,7 +1827,7 @@ func TestGitCommandDiff(t *testing.T) {
 	}
 }
 
-// TestGitCommandGetMergeBase is a function
+// TestGitCommandGetMergeBase is a function.
 func TestGitCommandGetMergeBase(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -1917,7 +1917,7 @@ func TestGitCommandGetMergeBase(t *testing.T) {
 	}
 }
 
-// TestGitCommandCurrentBranchName is a function
+// TestGitCommandCurrentBranchName is a function.
 func TestGitCommandCurrentBranchName(t *testing.T) {
 	type scenario struct {
 		testName string
diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go
index 82264ecef..ebb855cbe 100644
--- a/pkg/commands/os_test.go
+++ b/pkg/commands/os_test.go
@@ -29,7 +29,7 @@ func newDummyAppConfig() *config.AppConfig {
 	return appConfig
 }
 
-// TestOSCommandRunCommandWithOutput is a function
+// TestOSCommandRunCommandWithOutput is a function.
 func TestOSCommandRunCommandWithOutput(t *testing.T) {
 	type scenario struct {
 		command string
@@ -57,7 +57,7 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
 	}
 }
 
-// TestOSCommandRunCommand is a function
+// TestOSCommandRunCommand is a function.
 func TestOSCommandRunCommand(t *testing.T) {
 	type scenario struct {
 		command string
@@ -78,7 +78,7 @@ func TestOSCommandRunCommand(t *testing.T) {
 	}
 }
 
-// TestOSCommandOpenFile is a function
+// TestOSCommandOpenFile is a function.
 func TestOSCommandOpenFile(t *testing.T) {
 	type scenario struct {
 		filename string
@@ -129,7 +129,7 @@ func TestOSCommandOpenFile(t *testing.T) {
 	}
 }
 
-// TestOSCommandEditFile is a function
+// TestOSCommandEditFile is a function.
 func TestOSCommandEditFile(t *testing.T) {
 	type scenario struct {
 		filename           string
@@ -259,7 +259,7 @@ func TestOSCommandEditFile(t *testing.T) {
 	}
 }
 
-// TestOSCommandQuote is a function
+// TestOSCommandQuote is a function.
 func TestOSCommandQuote(t *testing.T) {
 	osCommand := newDummyOSCommand()
 
@@ -296,7 +296,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
 	assert.EqualValues(t, expected, actual)
 }
 
-// TestOSCommandUnquote is a function
+// TestOSCommandUnquote is a function.
 func TestOSCommandUnquote(t *testing.T) {
 	osCommand := newDummyOSCommand()
 
@@ -307,7 +307,7 @@ func TestOSCommandUnquote(t *testing.T) {
 	assert.EqualValues(t, expected, actual)
 }
 
-// TestOSCommandFileType is a function
+// TestOSCommandFileType is a function.
 func TestOSCommandFileType(t *testing.T) {
 	type scenario struct {
 		path  string
diff --git a/pkg/commands/pull_request_test.go b/pkg/commands/pull_request_test.go
index a8d0ac608..774baf92e 100644
--- a/pkg/commands/pull_request_test.go
+++ b/pkg/commands/pull_request_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-// TestGetRepoInfoFromURL is a function
+// TestGetRepoInfoFromURL is a function.
 func TestGetRepoInfoFromURL(t *testing.T) {
 	type scenario struct {
 		testName string
@@ -42,7 +42,7 @@ func TestGetRepoInfoFromURL(t *testing.T) {
 	}
 }
 
-// TestCreatePullRequest is a function
+// TestCreatePullRequest is a function.
 func TestCreatePullRequest(t *testing.T) {
 	type scenario struct {
 		testName string
diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go
index 6f828312c..809442481 100644
--- a/pkg/gui/commit_message_panel.go
+++ b/pkg/gui/commit_message_panel.go
@@ -82,7 +82,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
 	return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
 }
 
-// RenderCommitLength is a function
+// RenderCommitLength is a function.
 func (gui *Gui) RenderCommitLength() {
 	if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
 		return
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 6c1f6c296..1b656659d 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -21,7 +21,7 @@ func (b *Binding) GetDisplayStrings() []string {
 	return []string{b.GetKey(), b.Description}
 }
 
-// GetKey is a function
+// GetKey is a function.
 func (b *Binding) GetKey() string {
 	r, ok := b.Key.(rune)
 	key := ""
@@ -35,7 +35,7 @@ func (b *Binding) GetKey() string {
 	return key
 }
 
-// GetKeybindings is a function
+// GetKeybindings is a function.
 func (gui *Gui) GetKeybindings() []*Binding {
 	bindings := []*Binding{
 		{
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index a146166bf..98da6a9c2 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -14,7 +14,7 @@ type recentRepo struct {
 	path string
 }
 
-// GetDisplayStrings is a function
+// GetDisplayStrings is a function.
 func (r *recentRepo) GetDisplayStrings() []string {
 	yellow := color.New(color.FgMagenta)
 	base := filepath.Base(r.path)
diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go
index 7ad5670d9..0739d6dc6 100644
--- a/pkg/i18n/i18n_test.go
+++ b/pkg/i18n/i18n_test.go
@@ -16,12 +16,13 @@ func getDummyLog() *logrus.Entry {
 	log.Out = ioutil.Discard
 	return log.WithField("test", "test")
 }
-// TestNewLocalizer is a function
+
+// TestNewLocalizer is a function.
 func TestNewLocalizer(t *testing.T) {
 	assert.NotNil(t, NewLocalizer(getDummyLog()))
 }
 
-// TestDetectLanguage is a function
+// TestDetectLanguage is a function.
 func TestDetectLanguage(t *testing.T) {
 	type scenario struct {
 		langDetector func() (string, error)
@@ -48,7 +49,7 @@ func TestDetectLanguage(t *testing.T) {
 	}
 }
 
-// TestLocalizer is a function
+// TestLocalizer is a function.
 func TestLocalizer(t *testing.T) {
 	type scenario struct {
 		userLang string
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index de86eaef8..41774051b 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-// TestSplitLines is a function
+// TestSplitLines is a function.
 func TestSplitLines(t *testing.T) {
 	type scenario struct {
 		multilineString string
@@ -37,7 +37,7 @@ func TestSplitLines(t *testing.T) {
 	}
 }
 
-// TestWithPadding is a function
+// TestWithPadding is a function.
 func TestWithPadding(t *testing.T) {
 	type scenario struct {
 		str      string
@@ -63,7 +63,7 @@ func TestWithPadding(t *testing.T) {
 	}
 }
 
-// TestTrimTrailingNewline is a function
+// TestTrimTrailingNewline is a function.
 func TestTrimTrailingNewline(t *testing.T) {
 	type scenario struct {
 		str      string
@@ -86,7 +86,7 @@ func TestTrimTrailingNewline(t *testing.T) {
 	}
 }
 
-// TestNormalizeLinefeeds is a function
+// TestNormalizeLinefeeds is a function.
 func TestNormalizeLinefeeds(t *testing.T) {
 	type scenario struct {
 		byteArray []byte
@@ -120,7 +120,7 @@ func TestNormalizeLinefeeds(t *testing.T) {
 	}
 }
 
-// TestResolvePlaceholderString is a function
+// TestResolvePlaceholderString is a function.
 func TestResolvePlaceholderString(t *testing.T) {
 	type scenario struct {
 		templateString string
@@ -174,7 +174,7 @@ func TestResolvePlaceholderString(t *testing.T) {
 	}
 }
 
-// TestDisplayArraysAligned is a function
+// TestDisplayArraysAligned is a function.
 func TestDisplayArraysAligned(t *testing.T) {
 	type scenario struct {
 		input    [][]string
@@ -203,12 +203,12 @@ type myDisplayable struct {
 
 type myStruct struct{}
 
-// GetDisplayStrings is a function
+// GetDisplayStrings is a function.
 func (d *myDisplayable) GetDisplayStrings() []string {
 	return d.strings
 }
 
-// TestGetDisplayStringArrays is a function
+// TestGetDisplayStringArrays is a function.
 func TestGetDisplayStringArrays(t *testing.T) {
 	type scenario struct {
 		input    []Displayable
@@ -230,7 +230,7 @@ func TestGetDisplayStringArrays(t *testing.T) {
 	}
 }
 
-// TestRenderDisplayableList is a function
+// TestRenderDisplayableList is a function.
 func TestRenderDisplayableList(t *testing.T) {
 	type scenario struct {
 		input          []Displayable
@@ -272,7 +272,7 @@ func TestRenderDisplayableList(t *testing.T) {
 	}
 }
 
-// TestRenderList is a function
+// TestRenderList is a function.
 func TestRenderList(t *testing.T) {
 	type scenario struct {
 		input          interface{}
@@ -311,7 +311,7 @@ func TestRenderList(t *testing.T) {
 	}
 }
 
-// TestGetPaddedDisplayStrings is a function
+// TestGetPaddedDisplayStrings is a function.
 func TestGetPaddedDisplayStrings(t *testing.T) {
 	type scenario struct {
 		stringArrays [][]string
@@ -332,7 +332,7 @@ func TestGetPaddedDisplayStrings(t *testing.T) {
 	}
 }
 
-// TestGetPadWidths is a function
+// TestGetPadWidths is a function.
 func TestGetPadWidths(t *testing.T) {
 	type scenario struct {
 		stringArrays [][]string
@@ -359,7 +359,7 @@ func TestGetPadWidths(t *testing.T) {
 	}
 }
 
-// TestMin is a function
+// TestMin is a function.
 func TestMin(t *testing.T) {
 	type scenario struct {
 		a        int
@@ -390,7 +390,7 @@ func TestMin(t *testing.T) {
 	}
 }
 
-// TestIncludesString is a function
+// TestIncludesString is a function.
 func TestIncludesString(t *testing.T) {
 	type scenario struct {
 		list     []string

From 99824c8a7b840cc2fa8c06f248acfaf01a3f964b Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Sun, 2 Dec 2018 17:29:17 +1100
Subject: [PATCH 5/8] add patch modifier struct

---
 pkg/git/patch_modifier.go             | 110 ++++++++++++++++++++++++++
 pkg/git/patch_modifier_test.go        |  68 ++++++++++++++++
 pkg/git/testdata/testPatchAfter1.diff |  13 +++
 pkg/git/testdata/testPatchAfter2.diff |  14 ++++
 pkg/git/testdata/testPatchBefore.diff |  15 ++++
 5 files changed, 220 insertions(+)
 create mode 100644 pkg/git/patch_modifier.go
 create mode 100644 pkg/git/patch_modifier_test.go
 create mode 100644 pkg/git/testdata/testPatchAfter1.diff
 create mode 100644 pkg/git/testdata/testPatchAfter2.diff
 create mode 100644 pkg/git/testdata/testPatchBefore.diff

diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
new file mode 100644
index 000000000..0e31af952
--- /dev/null
+++ b/pkg/git/patch_modifier.go
@@ -0,0 +1,110 @@
+package git
+
+import (
+	"errors"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+)
+
+type PatchModifier struct {
+	Log *logrus.Entry
+}
+
+// NewPatchModifier builds a new branch list builder
+func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
+	return &PatchModifier{
+		Log: log,
+	}, nil
+}
+
+// ModifyPatch takes the original patch, which may contain several hunks,
+// and the line number of the line we want to stage
+func (p *PatchModifier) ModifyPatch(patch string, lineNumber int) (string, error) {
+	lines := strings.Split(patch, "\n")
+	headerLength := 4
+	output := strings.Join(lines[0:headerLength], "\n") + "\n"
+
+	hunkStart, err := p.getHunkStart(lines, lineNumber)
+	if err != nil {
+		return "", err
+	}
+
+	hunk, err := p.getModifiedHunk(lines, hunkStart, lineNumber)
+	if err != nil {
+		return "", err
+	}
+
+	output += strings.Join(hunk, "\n")
+
+	return output, nil
+}
+
+// getHunkStart returns the line number of the hunk we're going to be modifying
+// in order to stage our line
+func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int, error) {
+	// find the hunk that we're modifying
+	hunkStart := 0
+	for index, line := range patchLines {
+		if strings.HasPrefix(line, "@@") {
+			hunkStart = index
+		}
+		if index == lineNumber {
+			return hunkStart, nil
+		}
+	}
+	return 0, errors.New("Could not find hunk")
+}
+
+func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
+	lineChanges := 0
+	// strip the hunk down to just the line we want to stage
+	newHunk := []string{}
+	for offsetIndex, line := range patchLines[hunkStart:] {
+		index := offsetIndex + hunkStart
+		if index != lineNumber {
+			// we include other removals but treat them like context
+			if strings.HasPrefix(line, "-") {
+				newHunk = append(newHunk, " "+line[1:])
+				lineChanges += 1
+				continue
+			}
+			// we don't include other additions
+			if strings.HasPrefix(line, "+") {
+				lineChanges -= 1
+				continue
+			}
+		}
+		newHunk = append(newHunk, line)
+	}
+
+	var err error
+	newHunk[0], err = p.updatedHeader(newHunk[0], lineChanges)
+	if err != nil {
+		return nil, err
+	}
+
+	return newHunk, nil
+}
+
+// updatedHeader returns the hunk header with the updated line range
+// we need to update the hunk length to reflect the changes we made
+// if the hunk has three additions but we're only staging one, then
+// @@ -14,8 +14,11 @@ import (
+// becomes
+// @@ -14,8 +14,9 @@ import (
+func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
+	// current counter is the number after the second comma
+	re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
+	prevLengthString := re.FindStringSubmatch(currentHeader)[1]
+
+	prevLength, err := strconv.Atoi(prevLengthString)
+	if err != nil {
+		return "", err
+	}
+	re = regexp.MustCompile(`\d+ @@`)
+	newLength := strconv.Itoa(prevLength + lineChanges)
+	return re.ReplaceAllString(currentHeader, newLength+" @@"), nil
+}
diff --git a/pkg/git/patch_modifier_test.go b/pkg/git/patch_modifier_test.go
new file mode 100644
index 000000000..af7be3751
--- /dev/null
+++ b/pkg/git/patch_modifier_test.go
@@ -0,0 +1,68 @@
+package git
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+)
+
+func newDummyLog() *logrus.Entry {
+	log := logrus.New()
+	log.Out = ioutil.Discard
+	return log.WithField("test", "test")
+}
+
+func newDummyPatchModifier() *PatchModifier {
+	return &PatchModifier{
+		Log: newDummyLog(),
+	}
+}
+func TestModifyPatch(t *testing.T) {
+	type scenario struct {
+		testName              string
+		patchFilename         string
+		lineNumber            int
+		shouldError           bool
+		expectedPatchFilename string
+	}
+
+	scenarios := []scenario{
+		{
+			"Removing one line",
+			"testdata/testPatchBefore.diff",
+			8,
+			false,
+			"testdata/testPatchAfter1.diff",
+		},
+		{
+			"Adding one line",
+			"testdata/testPatchBefore.diff",
+			10,
+			false,
+			"testdata/testPatchAfter2.diff",
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			p := newDummyPatchModifier()
+			beforePatch, err := ioutil.ReadFile(s.patchFilename)
+			if err != nil {
+				panic("Cannot open file at " + s.patchFilename)
+			}
+			afterPatch, err := p.ModifyPatch(string(beforePatch), s.lineNumber)
+			if s.shouldError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				expected, err := ioutil.ReadFile(s.expectedPatchFilename)
+				if err != nil {
+					panic("Cannot open file at " + s.expectedPatchFilename)
+				}
+				assert.Equal(t, string(expected), afterPatch)
+			}
+		})
+	}
+}
diff --git a/pkg/git/testdata/testPatchAfter1.diff b/pkg/git/testdata/testPatchAfter1.diff
new file mode 100644
index 000000000..88066e1c2
--- /dev/null
+++ b/pkg/git/testdata/testPatchAfter1.diff
@@ -0,0 +1,13 @@
+diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
+index 60ec4e0..db4485d 100644
+--- a/pkg/git/branch_list_builder.go
++++ b/pkg/git/branch_list_builder.go
+@@ -14,8 +14,7 @@ import (
+ 
+ // context:
+ // we want to only show 'safe' branches (ones that haven't e.g. been deleted)
+-// which `git branch -a` gives us, but we also want the recency data that
+ // git reflog gives us.
+ // So we get the HEAD, then append get the reflog branches that intersect with
+ // our safe branches, then add the remaining safe branches, ensuring uniqueness
+ // along the way
diff --git a/pkg/git/testdata/testPatchAfter2.diff b/pkg/git/testdata/testPatchAfter2.diff
new file mode 100644
index 000000000..0a17c2b67
--- /dev/null
+++ b/pkg/git/testdata/testPatchAfter2.diff
@@ -0,0 +1,14 @@
+diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
+index 60ec4e0..db4485d 100644
+--- a/pkg/git/branch_list_builder.go
++++ b/pkg/git/branch_list_builder.go
+@@ -14,8 +14,9 @@ import (
+ 
+ // context:
+ // we want to only show 'safe' branches (ones that haven't e.g. been deleted)
+ // which `git branch -a` gives us, but we also want the recency data that
+ // git reflog gives us.
++// test 2 - if I remove this, I decrement the end counter
+ // So we get the HEAD, then append get the reflog branches that intersect with
+ // our safe branches, then add the remaining safe branches, ensuring uniqueness
+ // along the way
diff --git a/pkg/git/testdata/testPatchBefore.diff b/pkg/git/testdata/testPatchBefore.diff
new file mode 100644
index 000000000..14e4b0e23
--- /dev/null
+++ b/pkg/git/testdata/testPatchBefore.diff
@@ -0,0 +1,15 @@
+diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
+index 60ec4e0..db4485d 100644
+--- a/pkg/git/branch_list_builder.go
++++ b/pkg/git/branch_list_builder.go
+@@ -14,8 +14,8 @@ import (
+ 
+ // context:
+ // we want to only show 'safe' branches (ones that haven't e.g. been deleted)
+-// which `git branch -a` gives us, but we also want the recency data that
+-// git reflog gives us.
++// test 2 - if I remove this, I decrement the end counter
++// test
+ // So we get the HEAD, then append get the reflog branches that intersect with
+ // our safe branches, then add the remaining safe branches, ensuring uniqueness
+ // along the way

From 658e5a9faf8409c62f11f3ad6d636d0255e450f4 Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Sun, 2 Dec 2018 19:57:01 +1100
Subject: [PATCH 6/8] initial support for staging individual lines

---
 pkg/commands/git.go       |  33 +++++++-
 pkg/commands/git_test.go  |   2 +-
 pkg/git/patch_modifier.go |  17 +++-
 pkg/git/patch_parser.go   |  35 ++++++++
 pkg/gui/files_panel.go    |  22 ++++-
 pkg/gui/gui.go            |  22 +++++
 pkg/gui/keybindings.go    |  27 +++++++
 pkg/gui/staging_panel.go  | 163 ++++++++++++++++++++++++++++++++++++++
 pkg/gui/view_helpers.go   |  12 +++
 pkg/i18n/dutch.go         |   3 +
 pkg/i18n/english.go       |   9 +++
 pkg/i18n/polish.go        |   3 +
 12 files changed, 339 insertions(+), 9 deletions(-)
 create mode 100644 pkg/git/patch_parser.go
 create mode 100644 pkg/gui/staging_panel.go

diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index adc26b8c8..262a57c2f 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -3,6 +3,7 @@ package commands
 import (
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"strings"
@@ -571,9 +572,10 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
 }
 
 // Diff returns the diff of a file
-func (c *GitCommand) Diff(file *File) string {
+func (c *GitCommand) Diff(file *File, plain bool) string {
 	cachedArg := ""
 	trackedArg := "--"
+	colorArg := "--color"
 	fileName := c.OSCommand.Quote(file.Name)
 	if file.HasStagedChanges && !file.HasUnstagedChanges {
 		cachedArg = "--cached"
@@ -581,9 +583,36 @@ func (c *GitCommand) Diff(file *File) string {
 	if !file.Tracked && !file.HasStagedChanges {
 		trackedArg = "--no-index /dev/null"
 	}
-	command := fmt.Sprintf("git diff --color %s %s %s", cachedArg, trackedArg, fileName)
+	if plain {
+		colorArg = ""
+	}
+
+	command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
 
 	// for now we assume an error means the file was deleted
 	s, _ := c.OSCommand.RunCommandWithOutput(command)
 	return s
 }
+
+func (c *GitCommand) ApplyPatch(patch string) (string, error) {
+
+	content := []byte(patch)
+	tmpfile, err := ioutil.TempFile("", "patch")
+	if err != nil {
+		c.Log.Error(err)
+		return "", errors.New("Could not create patch file") // TODO: i18n
+	}
+
+	defer os.Remove(tmpfile.Name()) // clean up
+
+	if _, err := tmpfile.Write(content); err != nil {
+		c.Log.Error(err)
+		return "", err
+	}
+	if err := tmpfile.Close(); err != nil {
+		c.Log.Error(err)
+		return "", err
+	}
+
+	return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", tmpfile.Name()))
+}
diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go
index aec928ea5..0e1390535 100644
--- a/pkg/commands/git_test.go
+++ b/pkg/commands/git_test.go
@@ -1822,7 +1822,7 @@ func TestGitCommandDiff(t *testing.T) {
 		t.Run(s.testName, func(t *testing.T) {
 			gitCmd := newDummyGitCommand()
 			gitCmd.OSCommand.command = s.command
-			gitCmd.Diff(s.file)
+			gitCmd.Diff(s.file, false)
 		})
 	}
 }
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index 0e31af952..05afcf5ba 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -61,9 +61,13 @@ func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int,
 func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
 	lineChanges := 0
 	// strip the hunk down to just the line we want to stage
-	newHunk := []string{}
-	for offsetIndex, line := range patchLines[hunkStart:] {
-		index := offsetIndex + hunkStart
+	newHunk := []string{patchLines[hunkStart]}
+	for offsetIndex, line := range patchLines[hunkStart+1:] {
+		index := offsetIndex + hunkStart + 1
+		if strings.HasPrefix(line, "@@") {
+			newHunk = append(newHunk, "\n")
+			break
+		}
 		if index != lineNumber {
 			// we include other removals but treat them like context
 			if strings.HasPrefix(line, "-") {
@@ -98,7 +102,12 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
 func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
 	// current counter is the number after the second comma
 	re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
-	prevLengthString := re.FindStringSubmatch(currentHeader)[1]
+	matches := re.FindStringSubmatch(currentHeader)
+	if len(matches) < 2 {
+		re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
+		matches = re.FindStringSubmatch(currentHeader)
+	}
+	prevLengthString := matches[1]
 
 	prevLength, err := strconv.Atoi(prevLengthString)
 	if err != nil {
diff --git a/pkg/git/patch_parser.go b/pkg/git/patch_parser.go
new file mode 100644
index 000000000..8fa4d355b
--- /dev/null
+++ b/pkg/git/patch_parser.go
@@ -0,0 +1,35 @@
+package git
+
+import (
+	"strings"
+
+	"github.com/sirupsen/logrus"
+)
+
+type PatchParser struct {
+	Log *logrus.Entry
+}
+
+// NewPatchParser builds a new branch list builder
+func NewPatchParser(log *logrus.Entry) (*PatchParser, error) {
+	return &PatchParser{
+		Log: log,
+	}, nil
+}
+
+func (p *PatchParser) ParsePatch(patch string) ([]int, []int, error) {
+	lines := strings.Split(patch, "\n")
+	hunkStarts := []int{}
+	stageableLines := []int{}
+	headerLength := 4
+	for offsetIndex, line := range lines[headerLength:] {
+		index := offsetIndex + headerLength
+		if strings.HasPrefix(line, "@@") {
+			hunkStarts = append(hunkStarts, index)
+		}
+		if strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+") {
+			stageableLines = append(stageableLines, index)
+		}
+	}
+	return hunkStarts, stageableLines, nil
+}
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index a00bd2843..3404903ef 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -45,6 +45,25 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
 	return gui.GitCommand.StageFile(file.Name)
 }
 
+func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
+	stagingView, err := g.View("staging")
+	if err != nil {
+		return err
+	}
+	file, err := gui.getSelectedFile(g)
+	if err != nil {
+		if err != gui.Errors.ErrNoFiles {
+			return err
+		}
+		return nil
+	}
+	if !file.Tracked || !file.HasUnstagedChanges {
+		return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
+	}
+	gui.switchFocus(g, v, stagingView)
+	return gui.refreshStagingPanel()
+}
+
 func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
 	file, err := gui.getSelectedFile(g)
 	if err != nil {
@@ -188,12 +207,11 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
 	if err := gui.renderfilesOptions(g, file); err != nil {
 		return err
 	}
-	var content string
 	if file.HasMergeConflicts {
 		return gui.refreshMergePanel(g)
 	}
 
-	content = gui.GitCommand.Diff(file)
+	content := gui.GitCommand.Diff(file, false)
 	return gui.renderString(g, "main", content)
 }
 
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index ee8fb1165..67415c5ce 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -72,6 +72,13 @@ type Gui struct {
 	statusManager *statusManager
 }
 
+type stagingState struct {
+	StageableLines   []int
+	HunkStarts       []int
+	CurrentLineIndex int
+	Diff             string
+}
+
 type guiState struct {
 	Files             []*commands.File
 	Branches          []*commands.Branch
@@ -85,6 +92,7 @@ type guiState struct {
 	EditHistory       *stack.Stack
 	Platform          commands.Platform
 	Updating          bool
+	StagingState      *stagingState
 }
 
 // NewGui builds a new gui handler
@@ -208,6 +216,20 @@ func (gui *Gui) layout(g *gocui.Gui) error {
 		v.FgColor = gocui.ColorWhite
 	}
 
+	v, err = g.SetView("staging", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+		v.Title = gui.Tr.SLocalize("StagingTitle")
+		v.Wrap = true
+		v.Highlight = true
+		v.FgColor = gocui.ColorWhite
+		if _, err := g.SetViewOnBottom("staging"); err != nil {
+			return err
+		}
+	}
+
 	if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
 		if err != gocui.ErrUnknownView {
 			return err
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 1b656659d..2c47ab4f3 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -212,6 +212,13 @@ func (gui *Gui) GetKeybindings() []*Binding {
 			Modifier:    gocui.ModNone,
 			Handler:     gui.handleResetHard,
 			Description: gui.Tr.SLocalize("resetHard"),
+		}, {
+			ViewName:    "files",
+			Key:         gocui.KeyEnter,
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleSwitchToStagingPanel,
+			Description: gui.Tr.SLocalize("StageLines"),
+			KeyReadable: "enter",
 		}, {
 			ViewName: "main",
 			Key:      gocui.KeyEsc,
@@ -384,6 +391,26 @@ func (gui *Gui) GetKeybindings() []*Binding {
 			Key:      'q',
 			Modifier: gocui.ModNone,
 			Handler:  gui.handleMenuClose,
+		}, {
+			ViewName: "staging",
+			Key:      gocui.KeyEsc,
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingEscape,
+		}, {
+			ViewName: "staging",
+			Key:      gocui.KeyArrowUp,
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingKeyUp,
+		}, {
+			ViewName: "staging",
+			Key:      gocui.KeyArrowDown,
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingKeyDown,
+		}, {
+			ViewName: "staging",
+			Key:      gocui.KeySpace,
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStageLine,
 		},
 	}
 
diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go
new file mode 100644
index 000000000..be207c2fb
--- /dev/null
+++ b/pkg/gui/staging_panel.go
@@ -0,0 +1,163 @@
+package gui
+
+import (
+	"errors"
+	"io/ioutil"
+
+	"github.com/davecgh/go-spew/spew"
+
+	"github.com/jesseduffield/gocui"
+	"github.com/jesseduffield/lazygit/pkg/git"
+)
+
+func (gui *Gui) refreshStagingPanel() error {
+	// get the currently selected file. Get the diff of that file directly, not
+	// using any custom diff tools.
+	// parse the file to find out where the chunks and unstaged changes are
+
+	file, err := gui.getSelectedFile(gui.g)
+	if err != nil {
+		if err != gui.Errors.ErrNoFiles {
+			return err
+		}
+		return gui.handleStagingEscape(gui.g, nil)
+	}
+
+	if !file.HasUnstagedChanges {
+		return gui.handleStagingEscape(gui.g, nil)
+	}
+
+	// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
+	diff := gui.GitCommand.Diff(file, true)
+	colorDiff := gui.GitCommand.Diff(file, false)
+
+	gui.Log.WithField("staging", "staging").Info("DIFF IS:")
+	gui.Log.WithField("staging", "staging").Info(spew.Sdump(diff))
+	gui.Log.WithField("staging", "staging").Info("hello")
+
+	if len(diff) < 2 {
+		return gui.handleStagingEscape(gui.g, nil)
+	}
+
+	// parse the diff and store the line numbers of hunks and stageable lines
+	// TODO: maybe instantiate this at application start
+	p, err := git.NewPatchParser(gui.Log)
+	if err != nil {
+		return nil
+	}
+	hunkStarts, stageableLines, err := p.ParsePatch(diff)
+	if err != nil {
+		return nil
+	}
+
+	var currentLineIndex int
+	if gui.State.StagingState != nil {
+		end := len(stageableLines) - 1
+		if end < gui.State.StagingState.CurrentLineIndex {
+			currentLineIndex = end
+		} else {
+			currentLineIndex = gui.State.StagingState.CurrentLineIndex
+		}
+	} else {
+		currentLineIndex = 0
+	}
+
+	gui.State.StagingState = &stagingState{
+		StageableLines:   stageableLines,
+		HunkStarts:       hunkStarts,
+		CurrentLineIndex: currentLineIndex,
+		Diff:             diff,
+	}
+
+	if len(stageableLines) == 0 {
+		return errors.New("No lines to stage")
+	}
+
+	stagingView := gui.getStagingView(gui.g)
+	stagingView.SetCursor(0, stageableLines[currentLineIndex])
+	stagingView.SetOrigin(0, 0)
+	return gui.renderString(gui.g, "staging", colorDiff)
+}
+
+func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
+	if _, err := gui.g.SetViewOnBottom("staging"); err != nil {
+		return err
+	}
+
+	return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g))
+}
+
+// nextNumber returns the next index, cycling if we reach the end
+func nextIndex(numbers []int, currentNumber int) int {
+	for index, number := range numbers {
+		if number > currentNumber {
+			return index
+		}
+	}
+	return 0
+}
+
+// prevNumber returns the next number, cycling if we reach the end
+func prevIndex(numbers []int, currentNumber int) int {
+	end := len(numbers) - 1
+	for i := end; i >= 0; i -= 1 {
+		if numbers[i] < currentNumber {
+			return i
+		}
+	}
+	return end
+}
+
+func (gui *Gui) handleStagingKeyUp(g *gocui.Gui, v *gocui.View) error {
+	return gui.handleCycleLine(true)
+}
+
+func (gui *Gui) handleStagingKeyDown(g *gocui.Gui, v *gocui.View) error {
+	return gui.handleCycleLine(false)
+}
+
+func (gui *Gui) handleCycleLine(up bool) error {
+	state := gui.State.StagingState
+	lineNumbers := state.StageableLines
+	currentLine := lineNumbers[state.CurrentLineIndex]
+	var newIndex int
+	if up {
+		newIndex = prevIndex(lineNumbers, currentLine)
+	} else {
+		newIndex = nextIndex(lineNumbers, currentLine)
+	}
+
+	state.CurrentLineIndex = newIndex
+	stagingView := gui.getStagingView(gui.g)
+	stagingView.SetCursor(0, lineNumbers[newIndex])
+	stagingView.SetOrigin(0, 0)
+	return nil
+}
+
+func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
+	state := gui.State.StagingState
+	p, err := git.NewPatchModifier(gui.Log)
+	if err != nil {
+		return err
+	}
+
+	currentLine := state.StageableLines[state.CurrentLineIndex]
+	patch, err := p.ModifyPatch(state.Diff, currentLine)
+	if err != nil {
+		return err
+	}
+
+	// for logging purposes
+	ioutil.WriteFile("patch.diff", []byte(patch), 0600)
+
+	// apply the patch then refresh this panel
+	// create a new temp file with the patch, then call git apply with that patch
+	_, err = gui.GitCommand.ApplyPatch(patch)
+	if err != nil {
+		panic(err)
+	}
+
+	gui.refreshStagingPanel()
+	gui.refreshFiles(gui.g)
+	return nil
+}
diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go
index 6c3e5505c..e6970d92f 100644
--- a/pkg/gui/view_helpers.go
+++ b/pkg/gui/view_helpers.go
@@ -103,6 +103,9 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
 		return gui.handleCommitSelect(g, v)
 	case "stash":
 		return gui.handleStashEntrySelect(g, v)
+	case "staging":
+		return nil
+		// return gui.handleStagingSelect(g, v)
 	default:
 		panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
 	}
@@ -153,6 +156,10 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
 	if _, err := g.SetCurrentView(newView.Name()); err != nil {
 		return err
 	}
+	if _, err := g.SetViewOnTop(newView.Name()); err != nil {
+		return err
+	}
+
 	g.Cursor = newView.Editable
 
 	return gui.newLineFocused(g, newView)
@@ -293,6 +300,11 @@ func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View {
 	return v
 }
 
+func (gui *Gui) getStagingView(g *gocui.Gui) *gocui.View {
+	v, _ := g.View("staging")
+	return v
+}
+
 func (gui *Gui) trimmedContent(v *gocui.View) string {
 	return strings.TrimSpace(v.Buffer())
 }
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index 9ca18857b..b74d511e4 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -403,6 +403,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
 		}, &i18n.Message{
 			ID:    "NoBranchOnRemote",
 			Other: `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`,
+		}, &i18n.Message{
+			ID:    "StageLines",
+			Other: `stage individual hunks/lines`,
 		},
 	)
 }
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 9e0e60166..7a032e105 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -411,6 +411,15 @@ func addEnglish(i18nObject *i18n.Bundle) error {
 		}, &i18n.Message{
 			ID:    "NoBranchOnRemote",
 			Other: `This branch doesn't exist on remote. You need to push it to remote first.`,
+		}, &i18n.Message{
+			ID:    "StageLines",
+			Other: `stage individual hunks/lines`,
+		}, &i18n.Message{
+			ID:    "FileStagingRequirements",
+			Other: `Can only stage individual lines for tracked files with unstaged changes`,
+		}, &i18n.Message{
+			ID:    "StagingTitle",
+			Other: `Staging`,
 		},
 	)
 }
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 975035771..27035a18e 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -386,6 +386,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
 		}, &i18n.Message{
 			ID:    "NoBranchOnRemote",
 			Other: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
+		}, &i18n.Message{
+			ID:    "StageLines",
+			Other: `stage individual hunks/lines`,
 		},
 	)
 }

From c0f9795910dd840fb83e6992f7f59c77ec4c13fc Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Wed, 5 Dec 2018 19:33:46 +1100
Subject: [PATCH 7/8] staging lines and hunks

---
 pkg/commands/git.go                    |  21 +--
 pkg/commands/git_test.go               |  79 +++++++++++-
 pkg/commands/os.go                     |  26 ++++
 pkg/commands/os_test.go                |  32 +++++
 pkg/git/patch_modifier.go              |  61 +++++++--
 pkg/git/patch_modifier_test.go         |  25 +++-
 pkg/git/patch_parser.go                |   9 +-
 pkg/git/patch_parser_test.go           |  65 ++++++++++
 pkg/git/testdata/addedFile.diff        |   7 +
 pkg/git/testdata/testPatchAfter3.diff  |  25 ++++
 pkg/git/testdata/testPatchAfter4.diff  |  19 +++
 pkg/git/testdata/testPatchBefore2.diff |  57 ++++++++
 pkg/gui/confirmation_panel.go          |  12 +-
 pkg/gui/files_panel.go                 |   7 +-
 pkg/gui/gui.go                         |   1 -
 pkg/gui/keybindings.go                 |  55 ++++++--
 pkg/gui/staging_panel.go               | 172 ++++++++++++++++++-------
 pkg/gui/view_helpers.go                |   1 -
 pkg/i18n/english.go                    |  15 +++
 pkg/utils/utils.go                     |  21 +++
 pkg/utils/utils_test.go                |  92 +++++++++++++
 21 files changed, 703 insertions(+), 99 deletions(-)
 create mode 100644 pkg/git/patch_parser_test.go
 create mode 100644 pkg/git/testdata/addedFile.diff
 create mode 100644 pkg/git/testdata/testPatchAfter3.diff
 create mode 100644 pkg/git/testdata/testPatchAfter4.diff
 create mode 100644 pkg/git/testdata/testPatchBefore2.diff

diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index 262a57c2f..1d8e5a10d 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -3,7 +3,6 @@ package commands
 import (
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"os/exec"
 	"strings"
@@ -486,7 +485,6 @@ func (c *GitCommand) getMergeBase() (string, error) {
 	output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
 	if err != nil {
 		// swallowing error because it's not a big deal; probably because there are no commits yet
-		c.Log.Error(err)
 	}
 	return output, nil
 }
@@ -595,24 +593,13 @@ func (c *GitCommand) Diff(file *File, plain bool) string {
 }
 
 func (c *GitCommand) ApplyPatch(patch string) (string, error) {
-
-	content := []byte(patch)
-	tmpfile, err := ioutil.TempFile("", "patch")
+	filename, err := c.OSCommand.CreateTempFile("patch", patch)
 	if err != nil {
-		c.Log.Error(err)
-		return "", errors.New("Could not create patch file") // TODO: i18n
-	}
-
-	defer os.Remove(tmpfile.Name()) // clean up
-
-	if _, err := tmpfile.Write(content); err != nil {
-		c.Log.Error(err)
-		return "", err
-	}
-	if err := tmpfile.Close(); err != nil {
 		c.Log.Error(err)
 		return "", err
 	}
 
-	return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", tmpfile.Name()))
+	defer func() { _ = c.OSCommand.RemoveFile(filename) }()
+
+	return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename))
 }
diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go
index 0e1390535..206696b22 100644
--- a/pkg/commands/git_test.go
+++ b/pkg/commands/git_test.go
@@ -1770,6 +1770,7 @@ func TestGitCommandDiff(t *testing.T) {
 		testName string
 		command  func(string, ...string) *exec.Cmd
 		file     *File
+		plain    bool
 	}
 
 	scenarios := []scenario{
@@ -1786,6 +1787,22 @@ func TestGitCommandDiff(t *testing.T) {
 				HasStagedChanges: false,
 				Tracked:          true,
 			},
+			false,
+		},
+		{
+			"Default case",
+			func(cmd string, args ...string) *exec.Cmd {
+				assert.EqualValues(t, "git", cmd)
+				assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
+
+				return exec.Command("echo")
+			},
+			&File{
+				Name:             "test.txt",
+				HasStagedChanges: false,
+				Tracked:          true,
+			},
+			true,
 		},
 		{
 			"All changes staged",
@@ -1801,6 +1818,7 @@ func TestGitCommandDiff(t *testing.T) {
 				HasUnstagedChanges: false,
 				Tracked:            true,
 			},
+			false,
 		},
 		{
 			"File not tracked and file has no staged changes",
@@ -1815,6 +1833,7 @@ func TestGitCommandDiff(t *testing.T) {
 				HasStagedChanges: false,
 				Tracked:          false,
 			},
+			false,
 		},
 	}
 
@@ -1822,7 +1841,7 @@ func TestGitCommandDiff(t *testing.T) {
 		t.Run(s.testName, func(t *testing.T) {
 			gitCmd := newDummyGitCommand()
 			gitCmd.OSCommand.command = s.command
-			gitCmd.Diff(s.file, false)
+			gitCmd.Diff(s.file, s.plain)
 		})
 	}
 }
@@ -1979,3 +1998,61 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
 		})
 	}
 }
+
+func TestGitCommandApplyPatch(t *testing.T) {
+	type scenario struct {
+		testName string
+		command  func(string, ...string) *exec.Cmd
+		test     func(string, error)
+	}
+
+	scenarios := []scenario{
+		{
+			"valid case",
+			func(cmd string, args ...string) *exec.Cmd {
+				assert.Equal(t, "git", cmd)
+				assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
+				filename := args[2]
+				content, err := ioutil.ReadFile(filename)
+				assert.NoError(t, err)
+
+				assert.Equal(t, "test", string(content))
+
+				return exec.Command("echo", "done")
+			},
+			func(output string, err error) {
+				assert.NoError(t, err)
+				assert.EqualValues(t, "done\n", output)
+			},
+		},
+		{
+			"command returns error",
+			func(cmd string, args ...string) *exec.Cmd {
+				assert.Equal(t, "git", cmd)
+				assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
+				filename := args[2]
+				// TODO: Ideally we want to mock out OSCommand here so that we're not
+				// double handling testing it's CreateTempFile functionality,
+				// but it is going to take a bit of work to make a proper mock for it
+				// so I'm leaving it for another PR
+				content, err := ioutil.ReadFile(filename)
+				assert.NoError(t, err)
+
+				assert.Equal(t, "test", string(content))
+
+				return exec.Command("test")
+			},
+			func(output string, err error) {
+				assert.Error(t, err)
+			},
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			gitCmd := newDummyGitCommand()
+			gitCmd.OSCommand.command = s.command
+			s.test(gitCmd.ApplyPatch("test"))
+		})
+	}
+}
diff --git a/pkg/commands/os.go b/pkg/commands/os.go
index 8b4b7879e..6b28a69bb 100644
--- a/pkg/commands/os.go
+++ b/pkg/commands/os.go
@@ -2,6 +2,7 @@ package commands
 
 import (
 	"errors"
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"strings"
@@ -176,3 +177,28 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
 	_, err = f.WriteString("\n" + line)
 	return err
 }
+
+// CreateTempFile writes a string to a new temp file and returns the file's name
+func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
+	tmpfile, err := ioutil.TempFile("", filename)
+	if err != nil {
+		c.Log.Error(err)
+		return "", err
+	}
+
+	if _, err := tmpfile.Write([]byte(content)); err != nil {
+		c.Log.Error(err)
+		return "", err
+	}
+	if err := tmpfile.Close(); err != nil {
+		c.Log.Error(err)
+		return "", err
+	}
+
+	return tmpfile.Name(), nil
+}
+
+// RemoveFile removes a file at the specified path
+func (c *OSCommand) RemoveFile(filename string) error {
+	return os.Remove(filename)
+}
diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go
index ebb855cbe..a08c4b57d 100644
--- a/pkg/commands/os_test.go
+++ b/pkg/commands/os_test.go
@@ -1,6 +1,7 @@
 package commands
 
 import (
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"testing"
@@ -364,3 +365,34 @@ func TestOSCommandFileType(t *testing.T) {
 		_ = os.RemoveAll(s.path)
 	}
 }
+
+func TestOSCommandCreateTempFile(t *testing.T) {
+	type scenario struct {
+		testName string
+		filename string
+		content  string
+		test     func(string, error)
+	}
+
+	scenarios := []scenario{
+		{
+			"valid case",
+			"filename",
+			"content",
+			func(path string, err error) {
+				assert.NoError(t, err)
+
+				content, err := ioutil.ReadFile(path)
+				assert.NoError(t, err)
+
+				assert.Equal(t, "content", string(content))
+			},
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			s.test(newDummyOSCommand().CreateTempFile(s.filename, s.content))
+		})
+	}
+}
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index 05afcf5ba..3c523232e 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -6,11 +6,14 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/jesseduffield/lazygit/pkg/i18n"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 	"github.com/sirupsen/logrus"
 )
 
 type PatchModifier struct {
 	Log *logrus.Entry
+	Tr  *i18n.Localizer
 }
 
 // NewPatchModifier builds a new branch list builder
@@ -20,11 +23,49 @@ func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
 	}, nil
 }
 
-// ModifyPatch takes the original patch, which may contain several hunks,
-// and the line number of the line we want to stage
-func (p *PatchModifier) ModifyPatch(patch string, lineNumber int) (string, error) {
+// ModifyPatchForHunk takes the original patch, which may contain several hunks,
+// and removes any hunks that aren't the selected hunk
+func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, currentLine int) (string, error) {
+	// get hunk start and end
 	lines := strings.Split(patch, "\n")
-	headerLength := 4
+	hunkStartIndex := utils.PrevIndex(hunkStarts, currentLine)
+	hunkStart := hunkStarts[hunkStartIndex]
+	nextHunkStartIndex := utils.NextIndex(hunkStarts, currentLine)
+	var hunkEnd int
+	if nextHunkStartIndex == 0 {
+		hunkEnd = len(lines) - 1
+	} else {
+		hunkEnd = hunkStarts[nextHunkStartIndex]
+	}
+
+	headerLength, err := p.getHeaderLength(lines)
+	if err != nil {
+		return "", err
+	}
+
+	output := strings.Join(lines[0:headerLength], "\n") + "\n"
+	output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
+
+	return output, nil
+}
+
+func (p *PatchModifier) getHeaderLength(patchLines []string) (int, error) {
+	for index, line := range patchLines {
+		if strings.HasPrefix(line, "@@") {
+			return index, nil
+		}
+	}
+	return 0, errors.New(p.Tr.SLocalize("CantFindHunks"))
+}
+
+// ModifyPatchForLine takes the original patch, which may contain several hunks,
+// and the line number of the line we want to stage
+func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
+	lines := strings.Split(patch, "\n")
+	headerLength, err := p.getHeaderLength(lines)
+	if err != nil {
+		return "", err
+	}
 	output := strings.Join(lines[0:headerLength], "\n") + "\n"
 
 	hunkStart, err := p.getHunkStart(lines, lineNumber)
@@ -55,7 +96,8 @@ func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int,
 			return hunkStart, nil
 		}
 	}
-	return 0, errors.New("Could not find hunk")
+
+	return 0, errors.New(p.Tr.SLocalize("CantFindHunk"))
 }
 
 func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
@@ -101,13 +143,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
 // @@ -14,8 +14,9 @@ import (
 func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
 	// current counter is the number after the second comma
-	re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
-	matches := re.FindStringSubmatch(currentHeader)
-	if len(matches) < 2 {
-		re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
-		matches = re.FindStringSubmatch(currentHeader)
-	}
-	prevLengthString := matches[1]
+	re := regexp.MustCompile(`(\d+) @@`)
+	prevLengthString := re.FindStringSubmatch(currentHeader)[1]
 
 	prevLength, err := strconv.Atoi(prevLengthString)
 	if err != nil {
diff --git a/pkg/git/patch_modifier_test.go b/pkg/git/patch_modifier_test.go
index af7be3751..bc2073d55 100644
--- a/pkg/git/patch_modifier_test.go
+++ b/pkg/git/patch_modifier_test.go
@@ -19,7 +19,7 @@ func newDummyPatchModifier() *PatchModifier {
 		Log: newDummyLog(),
 	}
 }
-func TestModifyPatch(t *testing.T) {
+func TestModifyPatchForLine(t *testing.T) {
 	type scenario struct {
 		testName              string
 		patchFilename         string
@@ -43,6 +43,27 @@ func TestModifyPatch(t *testing.T) {
 			false,
 			"testdata/testPatchAfter2.diff",
 		},
+		{
+			"Adding one line in top hunk in diff with multiple hunks",
+			"testdata/testPatchBefore2.diff",
+			20,
+			false,
+			"testdata/testPatchAfter3.diff",
+		},
+		{
+			"Adding one line in top hunk in diff with multiple hunks",
+			"testdata/testPatchBefore2.diff",
+			53,
+			false,
+			"testdata/testPatchAfter4.diff",
+		},
+		{
+			"adding unstaged file with a single line",
+			"testdata/addedFile.diff",
+			6,
+			false,
+			"testdata/addedFile.diff",
+		},
 	}
 
 	for _, s := range scenarios {
@@ -52,7 +73,7 @@ func TestModifyPatch(t *testing.T) {
 			if err != nil {
 				panic("Cannot open file at " + s.patchFilename)
 			}
-			afterPatch, err := p.ModifyPatch(string(beforePatch), s.lineNumber)
+			afterPatch, err := p.ModifyPatchForLine(string(beforePatch), s.lineNumber)
 			if s.shouldError {
 				assert.Error(t, err)
 			} else {
diff --git a/pkg/git/patch_parser.go b/pkg/git/patch_parser.go
index 8fa4d355b..1dbacd01c 100644
--- a/pkg/git/patch_parser.go
+++ b/pkg/git/patch_parser.go
@@ -21,15 +21,16 @@ func (p *PatchParser) ParsePatch(patch string) ([]int, []int, error) {
 	lines := strings.Split(patch, "\n")
 	hunkStarts := []int{}
 	stageableLines := []int{}
-	headerLength := 4
-	for offsetIndex, line := range lines[headerLength:] {
-		index := offsetIndex + headerLength
+	pastHeader := false
+	for index, line := range lines {
 		if strings.HasPrefix(line, "@@") {
+			pastHeader = true
 			hunkStarts = append(hunkStarts, index)
 		}
-		if strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+") {
+		if pastHeader && (strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+")) {
 			stageableLines = append(stageableLines, index)
 		}
 	}
+	p.Log.WithField("staging", "staging").Info(stageableLines)
 	return hunkStarts, stageableLines, nil
 }
diff --git a/pkg/git/patch_parser_test.go b/pkg/git/patch_parser_test.go
new file mode 100644
index 000000000..6670aaea2
--- /dev/null
+++ b/pkg/git/patch_parser_test.go
@@ -0,0 +1,65 @@
+package git
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func newDummyPatchParser() *PatchParser {
+	return &PatchParser{
+		Log: newDummyLog(),
+	}
+}
+func TestParsePatch(t *testing.T) {
+	type scenario struct {
+		testName               string
+		patchFilename          string
+		shouldError            bool
+		expectedStageableLines []int
+		expectedHunkStarts     []int
+	}
+
+	scenarios := []scenario{
+		{
+			"Diff with one hunk",
+			"testdata/testPatchBefore.diff",
+			false,
+			[]int{8, 9, 10, 11},
+			[]int{4},
+		},
+		{
+			"Diff with two hunks",
+			"testdata/testPatchBefore2.diff",
+			false,
+			[]int{8, 9, 10, 11, 12, 13, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 34, 35, 36, 37, 45, 46, 47, 48, 49, 50, 51, 52, 53},
+			[]int{4, 41},
+		},
+		{
+			"Unstaged file",
+			"testdata/addedFile.diff",
+			false,
+			[]int{6},
+			[]int{5},
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			p := newDummyPatchParser()
+			beforePatch, err := ioutil.ReadFile(s.patchFilename)
+			if err != nil {
+				panic("Cannot open file at " + s.patchFilename)
+			}
+			hunkStarts, stageableLines, err := p.ParsePatch(string(beforePatch))
+			if s.shouldError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, s.expectedStageableLines, stageableLines)
+				assert.Equal(t, s.expectedHunkStarts, hunkStarts)
+			}
+		})
+	}
+}
diff --git a/pkg/git/testdata/addedFile.diff b/pkg/git/testdata/addedFile.diff
new file mode 100644
index 000000000..53966c4a1
--- /dev/null
+++ b/pkg/git/testdata/addedFile.diff
@@ -0,0 +1,7 @@
+diff --git a/blah b/blah
+new file mode 100644
+index 0000000..907b308
+--- /dev/null
++++ b/blah
+@@ -0,0 +1 @@
++blah
diff --git a/pkg/git/testdata/testPatchAfter3.diff b/pkg/git/testdata/testPatchAfter3.diff
new file mode 100644
index 000000000..03492450d
--- /dev/null
+++ b/pkg/git/testdata/testPatchAfter3.diff
@@ -0,0 +1,25 @@
+diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
+index a8fc600..6d8f7d7 100644
+--- a/pkg/git/patch_modifier.go
++++ b/pkg/git/patch_modifier.go
+@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
+ 		hunkEnd = hunkStarts[nextHunkStartIndex]
+ 	}
+ 
+ 	headerLength := 4
+ 	output := strings.Join(lines[0:headerLength], "\n") + "\n"
+ 	output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
+ 
+ 	return output, nil
+ }
+ 
++func getHeaderLength(patchLines []string) (int, error) {
+ // ModifyPatchForLine takes the original patch, which may contain several hunks,
+ // and the line number of the line we want to stage
+ func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
+ 	lines := strings.Split(patch, "\n")
+ 	headerLength := 4
+ 	output := strings.Join(lines[0:headerLength], "\n") + "\n"
+ 
+ 	hunkStart, err := p.getHunkStart(lines, lineNumber)
+
diff --git a/pkg/git/testdata/testPatchAfter4.diff b/pkg/git/testdata/testPatchAfter4.diff
new file mode 100644
index 000000000..99f894d9d
--- /dev/null
+++ b/pkg/git/testdata/testPatchAfter4.diff
@@ -0,0 +1,19 @@
+diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
+index a8fc600..6d8f7d7 100644
+--- a/pkg/git/patch_modifier.go
++++ b/pkg/git/patch_modifier.go
+@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
+ // @@ -14,8 +14,9 @@ import (
+ func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
+ 	// current counter is the number after the second comma
+ 	re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
+ 	matches := re.FindStringSubmatch(currentHeader)
+ 	if len(matches) < 2 {
+ 		re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
+ 		matches = re.FindStringSubmatch(currentHeader)
+ 	}
+ 	prevLengthString := matches[1]
++	prevLengthString := re.FindStringSubmatch(currentHeader)[1]
+ 
+ 	prevLength, err := strconv.Atoi(prevLengthString)
+ 	if err != nil {
diff --git a/pkg/git/testdata/testPatchBefore2.diff b/pkg/git/testdata/testPatchBefore2.diff
new file mode 100644
index 000000000..552c04f5e
--- /dev/null
+++ b/pkg/git/testdata/testPatchBefore2.diff
@@ -0,0 +1,57 @@
+diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
+index a8fc600..6d8f7d7 100644
+--- a/pkg/git/patch_modifier.go
++++ b/pkg/git/patch_modifier.go
+@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
+ 		hunkEnd = hunkStarts[nextHunkStartIndex]
+ 	}
+ 
+-	headerLength := 4
++	headerLength, err := getHeaderLength(lines)
++	if err != nil {
++		return "", err
++	}
++
+ 	output := strings.Join(lines[0:headerLength], "\n") + "\n"
+ 	output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
+ 
+ 	return output, nil
+ }
+ 
++func getHeaderLength(patchLines []string) (int, error) {
++	for index, line := range patchLines {
++		if strings.HasPrefix(line, "@@") {
++			return index, nil
++		}
++	}
++	return 0, errors.New("Could not find any hunks in this patch")
++}
++
+ // ModifyPatchForLine takes the original patch, which may contain several hunks,
+ // and the line number of the line we want to stage
+ func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
+ 	lines := strings.Split(patch, "\n")
+-	headerLength := 4
++	headerLength, err := getHeaderLength(lines)
++	if err != nil {
++		return "", err
++	}
+ 	output := strings.Join(lines[0:headerLength], "\n") + "\n"
+ 
+ 	hunkStart, err := p.getHunkStart(lines, lineNumber)
+@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
+ // @@ -14,8 +14,9 @@ import (
+ func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
+ 	// current counter is the number after the second comma
+-	re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
+-	matches := re.FindStringSubmatch(currentHeader)
+-	if len(matches) < 2 {
+-		re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
+-		matches = re.FindStringSubmatch(currentHeader)
+-	}
+-	prevLengthString := matches[1]
++	re := regexp.MustCompile(`(\d+) @@`)
++	prevLengthString := re.FindStringSubmatch(currentHeader)[1]
+ 
+ 	prevLength, err := strconv.Atoi(prevLengthString)
+ 	if err != nil {
diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go
index 58577e430..afe53ca2c 100644
--- a/pkg/gui/confirmation_panel.go
+++ b/pkg/gui/confirmation_panel.go
@@ -8,6 +8,7 @@ package gui
 
 import (
 	"strings"
+	"time"
 
 	"github.com/fatih/color"
 	"github.com/jesseduffield/gocui"
@@ -73,6 +74,7 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
 			return nil, err
 		}
 		confirmationView.Title = title
+		confirmationView.Wrap = true
 		confirmationView.FgColor = gocui.ColorWhite
 	}
 	confirmationView.Clear()
@@ -137,7 +139,15 @@ func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title,
 }
 
 func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
-	gui.Log.Error(message)
+	go func() {
+		// when reporting is switched on this log call sometimes introduces
+		// a delay on the error panel popping up. Here I'm adding a second wait
+		// so that the error is logged while the user is reading the error message
+		time.Sleep(time.Second)
+		gui.Log.Error(message)
+	}()
+
+	// gui.Log.WithField("staging", "staging").Info("creating confirmation panel")
 	currentView := g.CurrentView()
 	colorFunction := color.New(color.FgRed).SprintFunc()
 	coloredMessage := colorFunction(strings.TrimSpace(message))
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index 3404903ef..a16ef4909 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -57,10 +57,13 @@ func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
 		}
 		return nil
 	}
-	if !file.Tracked || !file.HasUnstagedChanges {
+	if !file.HasUnstagedChanges {
+		gui.Log.WithField("staging", "staging").Info("making error panel")
 		return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
 	}
-	gui.switchFocus(g, v, stagingView)
+	if err := gui.switchFocus(g, v, stagingView); err != nil {
+		return err
+	}
 	return gui.refreshStagingPanel()
 }
 
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 67415c5ce..9f25121d5 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -222,7 +222,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
 			return err
 		}
 		v.Title = gui.Tr.SLocalize("StagingTitle")
-		v.Wrap = true
 		v.Highlight = true
 		v.FgColor = gocui.ColorWhite
 		if _, err := g.SetViewOnBottom("staging"); err != nil {
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 2c47ab4f3..4158bedb7 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -392,25 +392,64 @@ func (gui *Gui) GetKeybindings() []*Binding {
 			Modifier: gocui.ModNone,
 			Handler:  gui.handleMenuClose,
 		}, {
-			ViewName: "staging",
-			Key:      gocui.KeyEsc,
-			Modifier: gocui.ModNone,
-			Handler:  gui.handleStagingEscape,
+			ViewName:    "staging",
+			Key:         gocui.KeyEsc,
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleStagingEscape,
+			KeyReadable: "esc",
+			Description: gui.Tr.SLocalize("EscapeStaging"),
 		}, {
 			ViewName: "staging",
 			Key:      gocui.KeyArrowUp,
 			Modifier: gocui.ModNone,
-			Handler:  gui.handleStagingKeyUp,
+			Handler:  gui.handleStagingPrevLine,
 		}, {
 			ViewName: "staging",
 			Key:      gocui.KeyArrowDown,
 			Modifier: gocui.ModNone,
-			Handler:  gui.handleStagingKeyDown,
+			Handler:  gui.handleStagingNextLine,
 		}, {
 			ViewName: "staging",
-			Key:      gocui.KeySpace,
+			Key:      'k',
 			Modifier: gocui.ModNone,
-			Handler:  gui.handleStageLine,
+			Handler:  gui.handleStagingPrevLine,
+		}, {
+			ViewName: "staging",
+			Key:      'j',
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingNextLine,
+		}, {
+			ViewName: "staging",
+			Key:      gocui.KeyArrowLeft,
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingPrevHunk,
+		}, {
+			ViewName: "staging",
+			Key:      gocui.KeyArrowRight,
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingNextHunk,
+		}, {
+			ViewName: "staging",
+			Key:      'h',
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingPrevHunk,
+		}, {
+			ViewName: "staging",
+			Key:      'l',
+			Modifier: gocui.ModNone,
+			Handler:  gui.handleStagingNextHunk,
+		}, {
+			ViewName:    "staging",
+			Key:         gocui.KeySpace,
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleStageLine,
+			Description: gui.Tr.SLocalize("StageLine"),
+		}, {
+			ViewName:    "staging",
+			Key:         'a',
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleStageHunk,
+			Description: gui.Tr.SLocalize("StageHunk"),
 		},
 	}
 
diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go
index be207c2fb..cba7d7638 100644
--- a/pkg/gui/staging_panel.go
+++ b/pkg/gui/staging_panel.go
@@ -2,19 +2,13 @@ package gui
 
 import (
 	"errors"
-	"io/ioutil"
-
-	"github.com/davecgh/go-spew/spew"
 
 	"github.com/jesseduffield/gocui"
 	"github.com/jesseduffield/lazygit/pkg/git"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
 func (gui *Gui) refreshStagingPanel() error {
-	// get the currently selected file. Get the diff of that file directly, not
-	// using any custom diff tools.
-	// parse the file to find out where the chunks and unstaged changes are
-
 	file, err := gui.getSelectedFile(gui.g)
 	if err != nil {
 		if err != gui.Errors.ErrNoFiles {
@@ -31,10 +25,6 @@ func (gui *Gui) refreshStagingPanel() error {
 	diff := gui.GitCommand.Diff(file, true)
 	colorDiff := gui.GitCommand.Diff(file, false)
 
-	gui.Log.WithField("staging", "staging").Info("DIFF IS:")
-	gui.Log.WithField("staging", "staging").Info(spew.Sdump(diff))
-	gui.Log.WithField("staging", "staging").Info("hello")
-
 	if len(diff) < 2 {
 		return gui.handleStagingEscape(gui.g, nil)
 	}
@@ -73,9 +63,9 @@ func (gui *Gui) refreshStagingPanel() error {
 		return errors.New("No lines to stage")
 	}
 
-	stagingView := gui.getStagingView(gui.g)
-	stagingView.SetCursor(0, stageableLines[currentLineIndex])
-	stagingView.SetOrigin(0, 0)
+	if err := gui.focusLineAndHunk(); err != nil {
+		return err
+	}
 	return gui.renderString(gui.g, "staging", colorDiff)
 }
 
@@ -84,57 +74,130 @@ func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
 		return err
 	}
 
+	gui.State.StagingState = nil
+
 	return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g))
 }
 
-// nextNumber returns the next index, cycling if we reach the end
-func nextIndex(numbers []int, currentNumber int) int {
-	for index, number := range numbers {
-		if number > currentNumber {
-			return index
-		}
-	}
-	return 0
-}
-
-// prevNumber returns the next number, cycling if we reach the end
-func prevIndex(numbers []int, currentNumber int) int {
-	end := len(numbers) - 1
-	for i := end; i >= 0; i -= 1 {
-		if numbers[i] < currentNumber {
-			return i
-		}
-	}
-	return end
-}
-
-func (gui *Gui) handleStagingKeyUp(g *gocui.Gui, v *gocui.View) error {
+func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
 	return gui.handleCycleLine(true)
 }
 
-func (gui *Gui) handleStagingKeyDown(g *gocui.Gui, v *gocui.View) error {
+func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error {
 	return gui.handleCycleLine(false)
 }
 
-func (gui *Gui) handleCycleLine(up bool) error {
+func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error {
+	return gui.handleCycleHunk(true)
+}
+
+func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error {
+	return gui.handleCycleHunk(false)
+}
+
+func (gui *Gui) handleCycleHunk(prev bool) error {
+	state := gui.State.StagingState
+	lineNumbers := state.StageableLines
+	currentLine := lineNumbers[state.CurrentLineIndex]
+	currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine)
+	var newHunkIndex int
+	if prev {
+		if currentHunkIndex == 0 {
+			newHunkIndex = len(state.HunkStarts) - 1
+		} else {
+			newHunkIndex = currentHunkIndex - 1
+		}
+	} else {
+		if currentHunkIndex == len(state.HunkStarts)-1 {
+			newHunkIndex = 0
+		} else {
+			newHunkIndex = currentHunkIndex + 1
+		}
+	}
+
+	state.CurrentLineIndex = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex])
+
+	return gui.focusLineAndHunk()
+}
+
+func (gui *Gui) handleCycleLine(prev bool) error {
 	state := gui.State.StagingState
 	lineNumbers := state.StageableLines
 	currentLine := lineNumbers[state.CurrentLineIndex]
 	var newIndex int
-	if up {
-		newIndex = prevIndex(lineNumbers, currentLine)
+	if prev {
+		newIndex = utils.PrevIndex(lineNumbers, currentLine)
 	} else {
-		newIndex = nextIndex(lineNumbers, currentLine)
+		newIndex = utils.NextIndex(lineNumbers, currentLine)
+	}
+	state.CurrentLineIndex = newIndex
+
+	return gui.focusLineAndHunk()
+}
+
+// focusLineAndHunk works out the best focus for the staging panel given the
+// selected line and size of the hunk
+func (gui *Gui) focusLineAndHunk() error {
+	stagingView := gui.getStagingView(gui.g)
+	state := gui.State.StagingState
+
+	lineNumber := state.StageableLines[state.CurrentLineIndex]
+
+	// we want the bottom line of the view buffer to ideally be the bottom line
+	// of the hunk, but if the hunk is too big we'll just go three lines beyond
+	// the currently selected line so that the user can see the context
+	var bottomLine int
+	nextHunkStartIndex := utils.NextIndex(state.HunkStarts, lineNumber)
+	if nextHunkStartIndex == 0 {
+		// for now linesHeight is an efficient means of getting the number of lines
+		// in the patch. However if we introduce word wrap we'll need to update this
+		bottomLine = stagingView.LinesHeight() - 1
+	} else {
+		bottomLine = state.HunkStarts[nextHunkStartIndex] - 1
 	}
 
-	state.CurrentLineIndex = newIndex
-	stagingView := gui.getStagingView(gui.g)
-	stagingView.SetCursor(0, lineNumbers[newIndex])
-	stagingView.SetOrigin(0, 0)
+	hunkStartIndex := utils.PrevIndex(state.HunkStarts, lineNumber)
+	hunkStart := state.HunkStarts[hunkStartIndex]
+	// if it's the first hunk we'll also show the diff header
+	if hunkStartIndex == 0 {
+		hunkStart = 0
+	}
+
+	_, height := stagingView.Size()
+	// if this hunk is too big, we will just ensure that the user can at least
+	// see three lines of context below the cursor
+	if bottomLine-hunkStart > height {
+		bottomLine = lineNumber + 3
+	}
+
+	return gui.focusLine(lineNumber, bottomLine, stagingView)
+}
+
+// focusLine takes a lineNumber to focus, and a bottomLine to ensure we can see
+func (gui *Gui) focusLine(lineNumber int, bottomLine int, v *gocui.View) error {
+	_, height := v.Size()
+	overScroll := bottomLine - height + 1
+	if overScroll < 0 {
+		overScroll = 0
+	}
+	if err := v.SetOrigin(0, overScroll); err != nil {
+		return err
+	}
+	if err := v.SetCursor(0, lineNumber-overScroll); err != nil {
+		return err
+	}
 	return nil
 }
 
+func (gui *Gui) handleStageHunk(g *gocui.Gui, v *gocui.View) error {
+	return gui.handleStageLineOrHunk(true)
+}
+
 func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
+	return gui.handleStageLineOrHunk(false)
+}
+
+func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
 	state := gui.State.StagingState
 	p, err := git.NewPatchModifier(gui.Log)
 	if err != nil {
@@ -142,22 +205,31 @@ func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
 	}
 
 	currentLine := state.StageableLines[state.CurrentLineIndex]
-	patch, err := p.ModifyPatch(state.Diff, currentLine)
+	var patch string
+	if hunk {
+		patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine)
+	} else {
+		patch, err = p.ModifyPatchForLine(state.Diff, currentLine)
+	}
 	if err != nil {
 		return err
 	}
 
 	// for logging purposes
-	ioutil.WriteFile("patch.diff", []byte(patch), 0600)
+	// ioutil.WriteFile("patch.diff", []byte(patch), 0600)
 
 	// apply the patch then refresh this panel
 	// create a new temp file with the patch, then call git apply with that patch
 	_, err = gui.GitCommand.ApplyPatch(patch)
 	if err != nil {
-		panic(err)
+		return err
 	}
 
-	gui.refreshStagingPanel()
-	gui.refreshFiles(gui.g)
+	if err := gui.refreshFiles(gui.g); err != nil {
+		return err
+	}
+	if err := gui.refreshStagingPanel(); err != nil {
+		return err
+	}
 	return nil
 }
diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go
index e6970d92f..525b05f97 100644
--- a/pkg/gui/view_helpers.go
+++ b/pkg/gui/view_helpers.go
@@ -259,7 +259,6 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
 		output := string(bom.Clean([]byte(s)))
 		output = utils.NormalizeLinefeeds(output)
 		fmt.Fprint(v, output)
-		v.Wrap = true
 		return nil
 	})
 	return nil
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 7a032e105..a3ce14d2e 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -420,6 +420,21 @@ func addEnglish(i18nObject *i18n.Bundle) error {
 		}, &i18n.Message{
 			ID:    "StagingTitle",
 			Other: `Staging`,
+		}, &i18n.Message{
+			ID:    "StageHunk",
+			Other: `stage hunk`,
+		}, &i18n.Message{
+			ID:    "StageLine",
+			Other: `stage line`,
+		}, &i18n.Message{
+			ID:    "EscapeStaging",
+			Other: `return to files panel`,
+		}, &i18n.Message{
+			ID:    "CantFindHunks",
+			Other: `Could not find any hunks in this patch`,
+		}, &i18n.Message{
+			ID:    "CantFindHunk",
+			Other: `Could not find hunk`,
 		},
 	)
 }
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 8e481b3a4..e2a5337e3 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -214,3 +214,24 @@ func IncludesString(list []string, a string) bool {
 	}
 	return false
 }
+
+// NextIndex returns the index of the element that comes after the given number
+func NextIndex(numbers []int, currentNumber int) int {
+	for index, number := range numbers {
+		if number > currentNumber {
+			return index
+		}
+	}
+	return 0
+}
+
+// PrevIndex returns the index that comes before the given number, cycling if we reach the end
+func PrevIndex(numbers []int, currentNumber int) int {
+	end := len(numbers) - 1
+	for i := end; i >= 0; i -= 1 {
+		if numbers[i] < currentNumber {
+			return i
+		}
+	}
+	return end
+}
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index 41774051b..f03bc087a 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -425,3 +425,95 @@ func TestIncludesString(t *testing.T) {
 		assert.EqualValues(t, s.expected, IncludesString(s.list, s.element))
 	}
 }
+
+func TestNextIndex(t *testing.T) {
+	type scenario struct {
+		testName string
+		list     []int
+		element  int
+		expected int
+	}
+
+	scenarios := []scenario{
+		{
+			// I'm not really fussed about how it behaves here
+			"no elements",
+			[]int{},
+			1,
+			0,
+		},
+		{
+			"one element",
+			[]int{1},
+			1,
+			0,
+		},
+		{
+			"two elements",
+			[]int{1, 2},
+			1,
+			1,
+		},
+		{
+			"two elements, giving second one",
+			[]int{1, 2},
+			2,
+			0,
+		},
+		{
+			"three elements, giving second one",
+			[]int{1, 2, 3},
+			2,
+			2,
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
+		})
+	}
+}
+
+func TestPrevIndex(t *testing.T) {
+	type scenario struct {
+		testName string
+		list     []int
+		element  int
+		expected int
+	}
+
+	scenarios := []scenario{
+		{
+			// I'm not really fussed about how it behaves here
+			"no elements",
+			[]int{},
+			1,
+			-1,
+		},
+		{
+			"one element",
+			[]int{1},
+			1,
+			0,
+		},
+		{
+			"two elements",
+			[]int{1, 2},
+			1,
+			1,
+		},
+		{
+			"three elements, giving second one",
+			[]int{1, 2, 3},
+			2,
+			0,
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
+		})
+	}
+}

From 933874fb25fdf9fbe3286d772285edfb44b4f9a2 Mon Sep 17 00:00:00 2001
From: Jesse Duffield 
Date: Wed, 5 Dec 2018 19:33:54 +1100
Subject: [PATCH 8/8] dutch and polish translations to be updated

---
 pkg/i18n/dutch.go  | 21 +++++++++++++++++++++
 pkg/i18n/polish.go | 21 +++++++++++++++++++++
 2 files changed, 42 insertions(+)

diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index b74d511e4..f49457ec4 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -406,6 +406,27 @@ func addDutch(i18nObject *i18n.Bundle) error {
 		}, &i18n.Message{
 			ID:    "StageLines",
 			Other: `stage individual hunks/lines`,
+		}, &i18n.Message{
+			ID:    "FileStagingRequirements",
+			Other: `Can only stage individual lines for tracked files with unstaged changes`,
+		}, &i18n.Message{
+			ID:    "StagingTitle",
+			Other: `Staging`,
+		}, &i18n.Message{
+			ID:    "StageHunk",
+			Other: `stage hunk`,
+		}, &i18n.Message{
+			ID:    "StageLine",
+			Other: `stage line`,
+		}, &i18n.Message{
+			ID:    "EscapeStaging",
+			Other: `return to files panel`,
+		}, &i18n.Message{
+			ID:    "CantFindHunks",
+			Other: `Could not find any hunks in this patch`,
+		}, &i18n.Message{
+			ID:    "CantFindHunk",
+			Other: `Could not find hunk`,
 		},
 	)
 }
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 27035a18e..3e1ce657d 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -389,6 +389,27 @@ func addPolish(i18nObject *i18n.Bundle) error {
 		}, &i18n.Message{
 			ID:    "StageLines",
 			Other: `stage individual hunks/lines`,
+		}, &i18n.Message{
+			ID:    "FileStagingRequirements",
+			Other: `Can only stage individual lines for tracked files with unstaged changes`,
+		}, &i18n.Message{
+			ID:    "StagingTitle",
+			Other: `Staging`,
+		}, &i18n.Message{
+			ID:    "StageHunk",
+			Other: `stage hunk`,
+		}, &i18n.Message{
+			ID:    "StageLine",
+			Other: `stage line`,
+		}, &i18n.Message{
+			ID:    "EscapeStaging",
+			Other: `return to files panel`,
+		}, &i18n.Message{
+			ID:    "CantFindHunks",
+			Other: `Could not find any hunks in this patch`,
+		}, &i18n.Message{
+			ID:    "CantFindHunk",
+			Other: `Could not find hunk`,
 		},
 	)
 }