From 4c18daf933b8c8ff2bec4ed6bc37c5a1db89a501 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 7 Nov 2026 10:19:50 +0100 Subject: [PATCH] Add a test for a lock error partway through skipping in a rebase If a lock stops "git rebase --skip" after git has applied the changes of the next commit but before it has committed them, git doesn't reschedule that commit. The changes stay staged. The git command runner then runs "git rebase --skip" again, and this discards the staged changes, so the commit is silently lost. Co-Authored-By: Claude Opus 5.5 --- .../lock_error_partway_through_skip.go | 76 +++++++++++++++++++ pkg/integration/tests/shared/index_lock.go | 12 +++ pkg/integration/tests/test_list.go | 1 + 3 files changed, 89 insertions(+) create mode 100644 pkg/integration/tests/interactive_rebase/lock_error_partway_through_skip.go diff --git a/pkg/integration/tests/interactive_rebase/lock_error_partway_through_skip.go b/pkg/integration/tests/interactive_rebase/lock_error_partway_through_skip.go new file mode 100644 index 000000000..c2865aff1 --- /dev/null +++ b/pkg/integration/tests/interactive_rebase/lock_error_partway_through_skip.go @@ -0,0 +1,76 @@ +package interactive_rebase + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var LockErrorPartwayThroughSkip = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Skip in a rebase while another git process takes index.lock before the next commit is committed", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + CreateFileAndAdd("file", "original\n"). + Commit("base"). + UpdateFileAndAdd("file", "changed\n"). + Commit("commit to edit"). + CreateFileAndAdd("file-y", "y\n"). + Commit("add file-y"). + CreateFileAndAdd("file-z", "z\n"). + Commit("add file-z") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("add file-z").IsSelected(), + Contains("add file-y"), + Contains("commit to edit"), + Contains("base"), + ). + NavigateToLine(Contains("commit to edit")). + Press(keys.Universal.Edit). + Lines( + Contains("─── Pending rebase todos"), + Contains("pick").Contains("add file-z"), + Contains("pick").Contains("add file-y"), + Contains("─── Commits"), + Contains("commit to edit").IsSelected(), + Contains("base"), + ) + + // After amending the commit we stopped at, git has to make a new commit + // when it picks "add file-y", rather than fast-forwarding to it. The + // lock then stops it after it has applied the changes, but before it + // has committed them. + t.Shell().RunCommand([]string{"git", "commit", "--amend", "-m", "edited commit"}) + shared.HoldIndexLockBeforeNextPickIsCommitted(t.Shell()) + + t.GlobalPress(keys.Universal.CreateRebaseOptionsMenu) + t.ExpectPopup().Menu(). + Title(Equals("Rebase options")). + Select(Contains("skip")). + Confirm() + + /* EXPECTED: + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("index.lock': File exists.")). + Confirm() + + t.Views().Files(). + Lines( + Contains("A").Contains("file-y"), + ) + ACTUAL: */ + t.Views().Commits(). + Lines( + Contains("add file-z"), + Contains("edited commit"), + Contains("base"), + ) + }, +}) diff --git a/pkg/integration/tests/shared/index_lock.go b/pkg/integration/tests/shared/index_lock.go index 29e513f6f..c8414813f 100644 --- a/pkg/integration/tests/shared/index_lock.go +++ b/pkg/integration/tests/shared/index_lock.go @@ -30,3 +30,15 @@ func HoldIndexLockAfterNextCommit(shell *Shell) { shell.CreateFile(".git/hooks/post-commit", "#!/bin/sh\n"+holdIndexLockScript) shell.MakeExecutable(".git/hooks/post-commit") } + +// HoldIndexLockBeforeNextPickIsCommitted installs a reference-transaction hook +// that takes index.lock (see holdIndexLockScript) the next time a rebase or +// cherry-pick has applied the changes of a commit, but hasn't committed them +// yet. This is when git creates CHERRY_PICK_HEAD. +func HoldIndexLockBeforeNextPickIsCommitted(shell *Shell) { + shell.CreateFile(".git/hooks/reference-transaction", `#!/bin/sh +[ "$1" = committed ] || exit 0 +awk '$3 == "CHERRY_PICK_HEAD" && $2 !~ /^0+$/ { found = 1 } END { exit !found }' || exit 0 +`+holdIndexLockScript) + shell.MakeExecutable(".git/hooks/reference-transaction") +} diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 21aec9a55..9dc3e4ff4 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -362,6 +362,7 @@ var tests = []*components.IntegrationTest{ interactive_rebase.InteractiveRebaseOfCopiedBranch, interactive_rebase.InteractiveRebaseWithConflictForEditCommand, interactive_rebase.LockErrorPartwayThroughRebase, + interactive_rebase.LockErrorPartwayThroughSkip, interactive_rebase.MidRebaseRangeSelect, interactive_rebase.Move, interactive_rebase.MoveAcrossBranchBoundaryOutsideRebase,