Repository: jesseduffield/lazygit (language: go)

Issue:
Enable squash/fixup of commits up to root level
while working in freshly cloned repos, we tend to make commits and squash/fixup up-to the first commit.

git supports this type of rebasing since [#1.7.12.](https://github.com/git/git/blob/master/Documentation/RelNotes/1.7.12.txt#L59)

```sh
git rebase -i --root master

pick sha1 X
squash sha1 Y
pick sha1 Z
```

Raising this feature request to see if this behaviour can be accomodated.



Discussion / hints:

Fix patch:
diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go
index 66dbcf45f6c..50ab4de7a34 100644
--- a/pkg/commands/git_commands/rebase.go
+++ b/pkg/commands/git_commands/rebase.go
@@ -130,7 +130,7 @@ func (self *RebaseCommands) InteractiveRebaseBreakAfter(commits []*models.Commit
 // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
 // we tell git to run lazygit to edit the todo list, and we pass the client
 // lazygit a todo string to write to the todo file
-func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
+func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseShaOrRoot string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
 	todo := self.buildTodo(todoLines)
 	ex := oscommands.GetLazygitPath()
 
@@ -139,7 +139,7 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
 		debug = "TRUE"
 	}
 
-	cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --no-autosquash %s", baseSha)
+	cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --no-autosquash %s", baseShaOrRoot)
 	self.Log.WithField("command", cmdStr).Debug("RunCommand")
 
 	cmdObj := self.cmd.New(cmdStr)
@@ -172,16 +172,8 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
 func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
 	baseIndex := actionIndex + 1
 
-	if len(commits) <= baseIndex {
-		return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
-	}
-
 	if action == "squash" || action == "fixup" {
 		baseIndex++
-
-		if len(commits) <= baseIndex {
-			return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
-		}
 	}
 
 	todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
@@ -197,16 +189,21 @@ func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, acti
 		}
 	})
 
-	return todoLines, commits[baseIndex].Sha, nil
+	baseSha := "--root"
+	if baseIndex < len(commits) {
+		baseSha = commits[baseIndex].Sha
+	}
+
+	return todoLines, baseSha, nil
 }
 
 // AmendTo amends the given commit with whatever files are staged
-func (self *RebaseCommands) AmendTo(sha string) error {
-	if err := self.commit.CreateFixupCommit(sha); err != nil {
+func (self *RebaseCommands) AmendTo(commit *models.Commit) error {
+	if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
 		return err
 	}
 
-	return self.SquashAllAboveFixupCommits(sha)
+	return self.SquashAllAboveFixupCommits(commit)
 }
 
 // EditRebaseTodo sets the action at a given index in the git-rebase-todo file
@@ -261,12 +258,17 @@ func (self *RebaseCommands) MoveTodoDown(index int) error {
 }
 
 // SquashAllAboveFixupCommits squashes all fixup! commits above the given one
-func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
+func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
+	shaOrRoot := commit.Sha + "^"
+	if commit.IsFirstCommit() {
+		shaOrRoot = "--root"
+	}
+
 	return self.runSkipEditorCommand(
 		self.cmd.New(
 			fmt.Sprintf(
-				"git rebase --interactive --rebase-merges --autostash --autosquash %s^",
-				sha,
+				"git rebase --interactive --rebase-merges --autostash --autosquash %s",
+				shaOrRoot,
 			),
 		),
 	)
diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go
index a9fe7a1905c..0cee212d122 100644
--- a/pkg/gui/controllers/local_commits_controller.go
+++ b/pkg/gui/controllers/local_commits_controller.go
@@ -148,8 +148,8 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
 }
 
 func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
-	if len(self.model.Commits) <= 1 {
-		return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
+	if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 {
+		return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
 	}
 
 	applied, err := self.handleMidRebaseCommand("squash", commit)
@@ -173,8 +173,8 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
 }
 
 func (self *LocalCommitsController) fixup(commit *models.Commit) error {
-	if len(self.model.Commits) <= 1 {
-		return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
+	if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 {
+		return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
 	}
 
 	applied, err := self.handleMidRebaseCommand("fixup", commit)
@@ -428,7 +428,7 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
 		HandleConfirm: func() error {
 			return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
 				self.c.LogAction(self.c.Tr.Actions.AmendCommit)
-				err := self.git.Rebase.AmendTo(commit.Sha)
+				err := self.git.Rebase.AmendTo(commit)
 				return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
 			})
 		},
@@ -571,7 +571,7 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
 		HandleConfirm: func() error {
 			return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
 				self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
-				err := self.git.Rebase.SquashAllAboveFixupCommits(commit.Sha)
+				err := self.git.Rebase.SquashAllAboveFixupCommits(commit)
 				return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
 			})
 		},
diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go
index 9533f0239dc..dbc8122e9f4 100644
--- a/pkg/i18n/chinese.go
+++ b/pkg/i18n/chinese.go
@@ -102,7 +102,7 @@ func chineseTranslationSet() TranslationSet {
 		LcSquashDown:                        "向下压缩",
 		LcFixupCommit:                       "修正提交（fixup）",
 		NoCommitsThisBranch:                 "该分支没有提交",
-		YouNoCommitsToSquash:                "您没有提交可以压缩",
+		CannotSquashOrFixupFirstCommit:      "There's no commit below to squash into",
 		Fixup:                               "修正（fixup）",
 		SureFixupThisCommit:                 "您确定要“修正”此提交吗？它将合并到下面的提交中",
 		SureSquashThisCommit:                "您确定要将这个提交压缩到下面的提交中吗？",
@@ -216,8 +216,6 @@ func chineseTranslationSet() TranslationSet {
 		LcPasteCommits:                      "粘贴提交（拣选）",
 		SureCherryPick:                      "您确定要将选中的提交进行拣选到这个分支吗？",
 		CherryPick:                          "拣选 (Cherry-Pick)",
-		CannotRebaseOntoFirstCommit:         "您不能以交互方式变基 (rebase) 至第一次提交",
-		CannotSquashOntoSecondCommit:        "您不能压缩 (squash) 或修正 (fixup) 第二个提交",
 		Donate:                              "捐助",
 		AskQuestion:                         "提问咨询",
 		PrevLine:                            "选择上一行",
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index 0127c07ad25..d8fcb5338ad 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -67,7 +67,7 @@ func dutchTranslationSet() TranslationSet {
 		LcQuit:                              "quit",
 		LcSquashDown:                        "squash beneden",
 		LcFixupCommit:                       "Fixup commit",
-		YouNoCommitsToSquash:                "Je hebt geen commits om mee te squashen",
+		CannotSquashOrFixupFirstCommit:      "There's no commit below to squash into",
 		Fixup:                               "Fixup",
 		SureFixupThisCommit:                 "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze",
 		SureSquashThisCommit:                "Weet je zeker dat je deze commit wil samenvoegen met de commit hieronder?",
@@ -181,8 +181,6 @@ func dutchTranslationSet() TranslationSet {
 		LcPasteCommits:                      "plak commits (cherry-pick)",
 		SureCherryPick:                      "Weet je zeker dat je de gekopieerde commits naar deze branch wil cherry-picken?",
 		CherryPick:                          "Cherry-Pick",
-		CannotRebaseOntoFirstCommit:         "Je kan niet interactief rebasen naar de eerste commit",
-		CannotSquashOntoSecondCommit:        "Je kan niet een squash/fixup doen naar de 2de commit",
 		Donate:                              "Doneer",
 		PrevLine:                            "selecteer de vorige lijn",
 		NextLine:                            "selecteer de volgende lijn",
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 06ffd9b1c4f..315851f3531 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -87,7 +87,7 @@ type TranslationSet struct {
 	LcQuit                              string
 	LcSquashDown                        string
 	LcFixupCommit                       string
-	YouNoCommitsToSquash                string
+	CannotSquashOrFixupFirstCommit      string
 	Fixup                               string
 	SureFixupThisCommit                 string
 	SureSquashThisCommit                string
@@ -219,8 +219,6 @@ type TranslationSet struct {
 	LcPasteCommits                      string
 	SureCherryPick                      string
 	CherryPick                          string
-	CannotRebaseOntoFirstCommit         string
-	CannotSquashOntoSecondCommit        string
 	Donate                              string
 	AskQuestion                         string
 	PrevLine                            string
@@ -737,7 +735,7 @@ func EnglishTranslationSet() TranslationSet {
 		LcSquashDown:                        "squash down",
 		LcFixupCommit:                       "fixup commit",
 		NoCommitsThisBranch:                 "No commits for this branch",
-		YouNoCommitsToSquash:                "You have no commits to squash with",
+		CannotSquashOrFixupFirstCommit:      "There's no commit below to squash into",
 		Fixup:                               "Fixup",
 		SureFixupThisCommit:                 "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
 		SureSquashThisCommit:                "Are you sure you want to squash this commit into the commit below?",
@@ -869,8 +867,6 @@ func EnglishTranslationSet() TranslationSet {
 		LcPasteCommits:                      "paste commits (cherry-pick)",
 		SureCherryPick:                      "Are you sure you want to cherry-pick the copied commits onto this branch?",
 		CherryPick:                          "Cherry-Pick",
-		CannotRebaseOntoFirstCommit:         "You cannot interactive rebase onto the first commit",
-		CannotSquashOntoSecondCommit:        "You cannot squash/fixup onto the second commit",
 		Donate:                              "Donate",
 		AskQuestion:                         "Ask Question",
 		PrevLine:                            "select previous line",
diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go
index 5248287a999..891d737b0e7 100644
--- a/pkg/i18n/japanese.go
+++ b/pkg/i18n/japanese.go
@@ -93,7 +93,7 @@ func japaneseTranslationSet() TranslationSet {
 		// LcSquashDown:                        "squash down",
 		// LcFixupCommit:                       "fixup commit",
 		// NoCommitsThisBranch:                 "No commits for this branch",
-		// YouNoCommitsToSquash:                "You have no commits to squash with",
+		// CannotSquashOrFixupFirstCommit:      "There's no commit below to squash into",
 		// Fixup:                               "Fixup",
 		// SureFixupThisCommit:                 "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
 		// SureSquashThisCommit:                "Are you sure you want to squash this commit into the commit below?",
@@ -215,9 +215,7 @@ func japaneseTranslationSet() TranslationSet {
 		LcCherryPickCopyRange: "コミットを範囲コピー (cherry-pick)",
 		LcPasteCommits:        "コミットを貼り付け (cherry-pick)",
 		// SureCherryPick:                      "Are you sure you want to cherry-pick the copied commits onto this branch?",
-		CherryPick: "Cherry-Pick",
-		// CannotRebaseOntoFirstCommit:         "You cannot interactive rebase onto the first commit",
-		// CannotSquashOntoSecondCommit:        "You cannot squash/fixup onto the second commit",
+		CherryPick:            "Cherry-Pick",
 		Donate:                "支援",
 		AskQuestion:           "質問",
 		PrevLine:              "前の行を選択",
diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go
index c191a436eab..fa649bcc0c9 100644
--- a/pkg/i18n/korean.go
+++ b/pkg/i18n/korean.go
@@ -92,7 +92,7 @@ func koreanTranslationSet() TranslationSet {
 		LcSquashDown:                        "squash down",
 		LcFixupCommit:                       "fixup commit",
 		NoCommitsThisBranch:                 "이 브랜치에 커밋이 없습니다.",
-		YouNoCommitsToSquash:                "You have no commits to squash with",
+		CannotSquashOrFixupFirstCommit:      "There's no commit below to squash into",
 		Fixup:                               "Fixup",
 		SureFixupThisCommit:                 "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
 		SureSquashThisCommit:                "Are you sure you want to squash this commit into the commit below?",
@@ -217,8 +217,6 @@ func koreanTranslationSet() TranslationSet {
 		LcPasteCommits:                      "커밋을 붙여넣기 (cherry-pick)",
 		SureCherryPick:                      "정말로 복사한 커밋을 이 브랜치에 체리픽하시겠습니까?",
 		CherryPick:                          "체리픽",
-		CannotRebaseOntoFirstCommit:         "첫 번째 커밋에 대해 대화식으로 리베이스할 수 없습니다.",
-		CannotSquashOntoSecondCommit:        "두 번째 커밋을 squash/fixup할 수 없습니다.",
 		Donate:                              "후원",
 		AskQuestion:                         "질문하기",
 		PrevLine:                            "이전 줄 선택",
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 05534c3654b..3db26a22c2d 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -62,7 +62,7 @@ func polishTranslationSet() TranslationSet {
 		LcSquashDown:                        "ściśnij",
 		LcFixupCommit:                       "napraw commit",
 		NoCommitsThisBranch:                 "Brak commitów dla tej gałęzi",
-		YouNoCommitsToSquash:                "Nie masz commitów do spłaszczenia",
+		CannotSquashOrFixupFirstCommit:      "There's no commit below to squash into",
 		Fixup:                               "Napraw",
 		SureFixupThisCommit:                 "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie spłaszczony w górę wraz z tym",
 		LcRewordCommit:                      "zmień nazwę commita",
@@ -149,8 +149,6 @@ func polishTranslationSet() TranslationSet {
 		LcPasteCommits:                      "wklej commity (przebieranie)",
 		SureCherryPick:                      "Czy na pewno chcesz przebierać w skopiowanych commitach na tej gałęzi?",
 		CherryPick:                          "Przebieranie",
-		CannotRebaseOntoFirstCommit:         "Nie można interaktywnie zmienić bazy na pierwszym commicie",
-		CannotSquashOntoSecondCommit:        "Nie można spłaszczyć na drugi commit",
 		Donate:                              "Wesprzyj",
 		PrevLine:                            "poprzednia linia",
 		NextLine:                            "następna linia",


Test patch:
diff --git a/pkg/integration/tests/interactive_rebase/amend_first_commit.go b/pkg/integration/tests/interactive_rebase/amend_first_commit.go
new file mode 100644
index 00000000000..d03a880b99b
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/amend_first_commit.go
@@ -0,0 +1,41 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var AmendFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Amends a staged file to the first (initial) commit.",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(2).
+			CreateFileAndAdd("fixup-file", "fixup content")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Commits.AmendToCommit).
+			Tap(func() {
+				t.ExpectPopup().Confirmation().
+					Title(Equals("Amend Commit")).
+					Content(Contains("Are you sure you want to amend this commit with your staged files?")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01").IsSelected(),
+			)
+
+		t.Views().Main().
+			Content(Contains("fixup content"))
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/edit_first_commit.go b/pkg/integration/tests/interactive_rebase/edit_first_commit.go
new file mode 100644
index 00000000000..def3b7fcab2
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/edit_first_commit.go
@@ -0,0 +1,38 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var EditFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Edits the first commit, just to show that it's possible",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(2)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Universal.Edit).
+			Lines(
+				Contains("commit 02"),
+				MatchesRegexp("YOU ARE HERE.*commit 01").IsSelected(),
+			).
+			Tap(func() {
+				t.Actions().ContinueRebase()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/fixup_first_commit.go b/pkg/integration/tests/interactive_rebase/fixup_first_commit.go
new file mode 100644
index 00000000000..fe339f3e217
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/fixup_first_commit.go
@@ -0,0 +1,37 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FixupFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Tries to fixup the first commit, which results in an error message",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(2)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Commits.MarkCommitAsFixup).
+			Tap(func() {
+				t.ExpectPopup().Alert().
+					Title(Equals("Error")).
+					Content(Equals("There's no commit below to squash into")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/fixup_second_commit.go b/pkg/integration/tests/interactive_rebase/fixup_second_commit.go
new file mode 100644
index 00000000000..007eba84fc6
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/fixup_second_commit.go
@@ -0,0 +1,44 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FixupSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Fixup the second commit into the first (initial)",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(3)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 03"),
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 02")).
+			Press(keys.Commits.MarkCommitAsFixup).
+			Tap(func() {
+				t.ExpectPopup().Confirmation().
+					Title(Equals("Fixup")).
+					Content(Equals("Are you sure you want to 'fixup' this commit? It will be merged into the commit below")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 03"),
+				Contains("commit 01").IsSelected(),
+			)
+
+		t.Views().Main().
+			Content(Contains("commit 01")).
+			Content(DoesNotContain("commit 02")).
+			Content(Contains("+file01 content")).
+			Content(Contains("+file02 content"))
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/reword_first_commit.go b/pkg/integration/tests/interactive_rebase/reword_first_commit.go
new file mode 100644
index 00000000000..9e631da090e
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/reword_first_commit.go
@@ -0,0 +1,39 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var RewordFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Rewords the first commit, just to show that it's possible",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(2)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Commits.RenameCommit).
+			Tap(func() {
+				t.ExpectPopup().Prompt().
+					Title(Equals("reword commit")).
+					InitialText(Equals("commit 01")).
+					Clear().
+					Type("renamed 01").
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("renamed 01"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go b/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go
new file mode 100644
index 00000000000..0ec334a6c85
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go
@@ -0,0 +1,37 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SquashDownFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Tries to squash down the first commit, which results in an error message",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(2)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Commits.SquashDown).
+			Tap(func() {
+				t.ExpectPopup().Alert().
+					Title(Equals("Error")).
+					Content(Equals("There's no commit below to squash into")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go b/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go
new file mode 100644
index 00000000000..ffa54584faf
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go
@@ -0,0 +1,43 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SquashDownSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Squash down the second commit into the first (initial)",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(3)
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 03"),
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 02")).
+			Press(keys.Commits.SquashDown).
+			Tap(func() {
+				t.ExpectPopup().Confirmation().
+					Title(Equals("Squash")).
+					Content(Equals("Are you sure you want to squash this commit into the commit below?")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 03"),
+				Contains("commit 01").IsSelected(),
+			)
+
+		t.Views().Main().
+			Content(Contains("    commit 01\n    \n    commit 02")).
+			Content(Contains("+file01 content")).
+			Content(Contains("+file02 content"))
+	},
+})
diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go b/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go
new file mode 100644
index 00000000000..ece1e5faecc
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go
@@ -0,0 +1,49 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Squashes all fixups above the first (initial) commit.",
+	ExtraCmdArgs: "",
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateNCommits(2).
+			CreateFileAndAdd("fixup-file", "fixup content")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+			).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Commits.CreateFixupCommit).
+			Tap(func() {
+				t.ExpectPopup().Confirmation().
+					Title(Equals("Create fixup commit")).
+					Content(Contains("Are you sure you want to create a fixup! commit for commit")).
+					Confirm()
+			}).
+			NavigateToListItem(Contains("commit 01")).
+			Press(keys.Commits.SquashAboveCommits).
+			Tap(func() {
+				t.ExpectPopup().Confirmation().
+					Title(Equals("Squash all 'fixup!' commits above selected commit (autosquash)")).
+					Content(Contains("Are you sure you want to squash all fixup! commits above")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01").IsSelected(),
+			)
+
+		t.Views().Main().
+			Content(Contains("fixup content"))
+	},
+})
diff --git a/pkg/integration/tests/tests_gen.go b/pkg/integration/tests/tests_gen.go
index 2be0b902c02..3bef44c7e80 100644
--- a/pkg/integration/tests/tests_gen.go
+++ b/pkg/integration/tests/tests_gen.go
@@ -65,8 +65,16 @@ var tests = []*components.IntegrationTest{
 	filter_by_path.CliArg,
 	filter_by_path.SelectFile,
 	filter_by_path.TypeFile,
+	interactive_rebase.AmendFirstCommit,
 	interactive_rebase.AmendMerge,
+	interactive_rebase.EditFirstCommit,
+	interactive_rebase.FixupFirstCommit,
+	interactive_rebase.FixupSecondCommit,
 	interactive_rebase.One,
+	interactive_rebase.RewordFirstCommit,
+	interactive_rebase.SquashDownFirstCommit,
+	interactive_rebase.SquashDownSecondCommit,
+	interactive_rebase.SquashFixupsAboveFirstCommit,
 	misc.ConfirmOnQuit,
 	misc.InitialOpen,
 	patch_building.CopyPatchToClipboard,