From 9462b881f614aca84e458ae4f9166de64b178b7f Mon Sep 17 00:00:00 2001 From: Alexander Slavschik Date: Wed, 4 Mar 2026 09:33:07 +0000 Subject: [PATCH 1/2] Convert skipHookPrefix to skipHookPrefixes (string to []string) --- docs-master/Config.md | 9 +- pkg/commands/git_commands/commit.go | 16 +- pkg/commands/git_commands/commit_test.go | 196 +++++++++++++----- pkg/config/app_config.go | 38 ++++ pkg/config/app_config_test.go | 96 ++++++++- pkg/config/user_config.go | 6 +- pkg/gui/context/commit_message_context.go | 15 +- pkg/gui/controllers/helpers/commits_helper.go | 6 +- .../helpers/working_tree_helper.go | 11 +- schema-master/config.json | 14 +- 10 files changed, 322 insertions(+), 85 deletions(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 9931fda61fe..1c11ab19de8 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -396,9 +396,12 @@ git: - master - main - # Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks - # will be skipped when the commit message starts with 'WIP' - skipHookPrefix: WIP + # Prefixes to use when skipping hooks. E.g. if set to ['WIP'], then pre-commit + # hooks will be skipped when the commit message starts with 'WIP'. The first + # entry in the array will be used for the "Commit changes without pre-commit + # hook" command. + skipHookPrefixes: + - WIP # If true, periodically fetch from remote autoFetch: true diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 40d2b7319ef..1c575f6692d 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -6,6 +6,7 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/samber/lo" ) var ErrInvalidCommitIndex = errors.New("invalid commit index") @@ -85,11 +86,16 @@ func (self *CommitCommands) ResetToCommit(hash string, strength string, envVars Run() } +func (self *CommitCommands) hasSkipHookPrefix(subject string) bool { + return lo.SomeBy(self.UserConfig().Git.SkipHookPrefixes, func(prefix string) bool { + return strings.HasPrefix(subject, prefix) + }) +} + func (self *CommitCommands) CommitCmdObj(summary string, description string, forceSkipHooks bool) *oscommands.CmdObj { messageArgs := self.commitMessageArgs(summary, description) - skipHookPrefix := self.UserConfig().Git.SkipHookPrefix cmdArgs := NewGitCmd("commit"). - ArgIf(forceSkipHooks || (skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix)), "--no-verify"). + ArgIf(forceSkipHooks || self.hasSkipHookPrefix(summary), "--no-verify"). ArgIf(self.signoffFlag() != "", self.signoffFlag()). Arg(messageArgs...). ToArgv() @@ -287,7 +293,10 @@ func (self *CommitCommands) Revert(hashes []string, isMerge bool) error { // CreateFixupCommit creates a commit that fixes up a previous commit func (self *CommitCommands) CreateFixupCommit(hash string) error { - cmdArgs := NewGitCmd("commit").Arg("--fixup=" + hash).ToArgv() + cmdArgs := NewGitCmd("commit"). + ArgIf(self.hasSkipHookPrefix("fixup! "), "--no-verify"). + Arg("--fixup=" + hash). + ToArgv() return self.cmd.New(cmdArgs).Run() } @@ -299,6 +308,7 @@ func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDe description += "\n\n" + newDescription } cmdArgs := NewGitCmd("commit"). + ArgIf(self.hasSkipHookPrefix("amend! "), "--no-verify"). Arg("-m", "amend! "+originalSubject). Arg("-m", description). ArgIf(!includeFileChanges, "--only", "--allow-empty"). diff --git a/pkg/commands/git_commands/commit_test.go b/pkg/commands/git_commands/commit_test.go index 6ea914c6402..67d2b8de3ae 100644 --- a/pkg/commands/git_commands/commit_test.go +++ b/pkg/commands/git_commands/commit_test.go @@ -51,72 +51,80 @@ func TestCommitResetToCommit(t *testing.T) { func TestCommitCommitCmdObj(t *testing.T) { type scenario struct { - testName string - summary string - forceSkipHooks bool - description string - configSignoff bool - configSkipHookPrefix string - expectedArgs []string + testName string + summary string + forceSkipHooks bool + description string + configSignoff bool + configSkipHookPrefixes []string + expectedArgs []string } scenarios := []scenario{ { - testName: "Commit", - summary: "test", - forceSkipHooks: false, - configSignoff: false, - configSkipHookPrefix: "", - expectedArgs: []string{"commit", "-m", "test"}, + testName: "Commit", + summary: "test", + forceSkipHooks: false, + configSignoff: false, + configSkipHookPrefixes: nil, + expectedArgs: []string{"commit", "-m", "test"}, }, { - testName: "Commit with --no-verify flag < only prefix", - summary: "WIP: test", - forceSkipHooks: false, - configSignoff: false, - configSkipHookPrefix: "WIP", - expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"}, + testName: "Commit with --no-verify flag < only prefix", + summary: "WIP: test", + forceSkipHooks: false, + configSignoff: false, + configSkipHookPrefixes: []string{"WIP"}, + expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"}, }, { - testName: "Commit with --no-verify flag < skip flag and prefix", - summary: "WIP: test", - forceSkipHooks: true, - configSignoff: false, - configSkipHookPrefix: "WIP", - expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"}, + testName: "Commit with --no-verify flag < skip flag and prefix", + summary: "WIP: test", + forceSkipHooks: true, + configSignoff: false, + configSkipHookPrefixes: []string{"WIP"}, + expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"}, }, { - testName: "Commit with --no-verify flag < skip flag no prefix", - summary: "test", - forceSkipHooks: true, - configSignoff: false, - configSkipHookPrefix: "WIP", - expectedArgs: []string{"commit", "--no-verify", "-m", "test"}, + testName: "Commit with --no-verify flag < skip flag no prefix", + summary: "test", + forceSkipHooks: true, + configSignoff: false, + configSkipHookPrefixes: []string{"WIP"}, + expectedArgs: []string{"commit", "--no-verify", "-m", "test"}, }, { - testName: "Commit with multiline message", - summary: "line1", - forceSkipHooks: false, - description: "line2", - configSignoff: false, - configSkipHookPrefix: "", - expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"}, + testName: "Commit with multiline message", + summary: "line1", + forceSkipHooks: false, + description: "line2", + configSignoff: false, + configSkipHookPrefixes: nil, + expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"}, }, { - testName: "Commit with signoff", - summary: "test", - forceSkipHooks: false, - configSignoff: true, - configSkipHookPrefix: "", - expectedArgs: []string{"commit", "--signoff", "-m", "test"}, + testName: "Commit with signoff", + summary: "test", + forceSkipHooks: false, + configSignoff: true, + configSkipHookPrefixes: nil, + expectedArgs: []string{"commit", "--signoff", "-m", "test"}, }, { - testName: "Commit with signoff and no-verify", - summary: "WIP: test", - forceSkipHooks: true, - configSignoff: true, - configSkipHookPrefix: "WIP", - expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"}, + testName: "Commit with signoff and no-verify", + summary: "WIP: test", + forceSkipHooks: true, + configSignoff: true, + configSkipHookPrefixes: []string{"WIP"}, + expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"}, + }, + { + testName: "Commit with multiple prefixes, second matches", + summary: "fixup! some commit", + forceSkipHooks: false, + configSignoff: false, + configSkipHookPrefixes: []string{"WIP", "fixup!", "squash!"}, + expectedArgs: []string{"commit", "--no-verify", "-m", "fixup! some commit"}, }, } @@ -124,7 +132,7 @@ func TestCommitCommitCmdObj(t *testing.T) { t.Run(s.testName, func(t *testing.T) { userConfig := config.GetDefaultConfig() userConfig.Git.Commit.SignOff = s.configSignoff - userConfig.Git.SkipHookPrefix = s.configSkipHookPrefix + userConfig.Git.SkipHookPrefixes = s.configSkipHookPrefixes runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil) instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner}) @@ -171,10 +179,11 @@ func TestCommitCommitEditorCmdObj(t *testing.T) { func TestCommitCreateFixupCommit(t *testing.T) { type scenario struct { - testName string - hash string - runner *oscommands.FakeCmdObjRunner - test func(error) + testName string + hash string + userConfig *config.UserConfig + runner *oscommands.FakeCmdObjRunner + test func(error) } scenarios := []scenario{ @@ -187,11 +196,47 @@ func TestCommitCreateFixupCommit(t *testing.T) { assert.NoError(t, err) }, }, + { + testName: "with matching skipHookPrefixes", + hash: "12345", + userConfig: &config.UserConfig{ + Git: config.GitConfig{SkipHookPrefixes: []string{"fixup!"}}, + }, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"commit", "--no-verify", "--fixup=12345"}, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + { + testName: "with non-matching skipHookPrefixes", + hash: "12345", + userConfig: &config.UserConfig{ + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP"}}, + }, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"commit", "--fixup=12345"}, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + { + testName: "with multiple prefixes including fixup!", + hash: "12345", + userConfig: &config.UserConfig{ + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP", "fixup!"}}, + }, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"commit", "--no-verify", "--fixup=12345"}, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, } for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { - instance := buildCommitCommands(commonDeps{runner: s.runner}) + instance := buildCommitCommands(commonDeps{runner: s.runner, userConfig: s.userConfig}) s.test(instance.CreateFixupCommit(s.hash)) s.runner.CheckForMissingCalls() }) @@ -205,6 +250,7 @@ func TestCommitCreateAmendCommit(t *testing.T) { newSubject string newDescription string includeFileChanges bool + userConfig *config.UserConfig runner *oscommands.FakeCmdObjRunner } @@ -236,11 +282,47 @@ func TestCommitCreateAmendCommit(t *testing.T) { runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject", "--only", "--allow-empty"}, "", nil), }, + { + testName: "with matching skipHookPrefixes", + originalSubject: "original subject", + newSubject: "new subject", + newDescription: "", + includeFileChanges: true, + userConfig: &config.UserConfig{ + Git: config.GitConfig{SkipHookPrefixes: []string{"amend!"}}, + }, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"commit", "--no-verify", "-m", "amend! original subject", "-m", "new subject"}, "", nil), + }, + { + testName: "with non-matching skipHookPrefixes", + originalSubject: "original subject", + newSubject: "new subject", + newDescription: "", + includeFileChanges: true, + userConfig: &config.UserConfig{ + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP"}}, + }, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject"}, "", nil), + }, + { + testName: "with multiple prefixes including amend!", + originalSubject: "original subject", + newSubject: "new subject", + newDescription: "", + includeFileChanges: true, + userConfig: &config.UserConfig{ + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP", "amend!"}}, + }, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"commit", "--no-verify", "-m", "amend! original subject", "-m", "new subject"}, "", nil), + }, } for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { - instance := buildCommitCommands(commonDeps{runner: s.runner}) + instance := buildCommitCommands(commonDeps{runner: s.runner, userConfig: s.userConfig}) err := instance.CreateAmendCommit(s.originalSubject, s.newSubject, s.newDescription, s.includeFileChanges) assert.NoError(t, err) s.runner.CheckForMissingCalls() diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 038d6c11785..68fbf976217 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -315,6 +315,11 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([] return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err) } + err = migrateSkipHookPrefix(&rootNode, changes) + if err != nil { + return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err) + } + // Add more migrations here... if reflect.DeepEqual(rootNode, originalCopy) { @@ -505,6 +510,39 @@ func migratePagers(rootNode *yaml.Node, changes *ChangesSet) error { }) } +func migrateSkipHookPrefix(rootNode *yaml.Node, changes *ChangesSet) error { + return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error { + keyNode, valueNode := yaml_utils.LookupKey(gitNode, "skipHookPrefix") + if keyNode == nil || valueNode.Kind != yaml.ScalarNode || valueNode.Tag != "!!str" { + return nil + } + + _, existingValueNode := yaml_utils.LookupKey(gitNode, "skipHookPrefixes") + if existingValueNode != nil { + return nil + } + + newKeyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "skipHookPrefixes", Tag: "!!str"} + newValueNodeContent := []*yaml.Node{} + if valueNode.Value != "" { + newValueNodeContent = []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: valueNode.Value, Tag: "!!str"}, + } + } + newValueNode := &yaml.Node{ + Kind: yaml.SequenceNode, + Tag: "!!seq", + Content: newValueNodeContent, + } + + gitNode.Content = append(gitNode.Content, newKeyNode, newValueNode) + _, _ = yaml_utils.RemoveKey(gitNode, "skipHookPrefix") + changes.Add("Moved git.skipHookPrefix string to git.skipHookPrefixes array") + + return nil + }) +} + func (c *AppConfig) GetDebug() bool { return c.debug } diff --git a/pkg/config/app_config_test.go b/pkg/config/app_config_test.go index 1109256a9d3..b409d89356e 100644 --- a/pkg/config/app_config_test.go +++ b/pkg/config/app_config_test.go @@ -645,8 +645,9 @@ git: - master - main - # Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks will be skipped when the commit message starts with 'WIP' - skipHookPrefix: WIP + # Prefixes to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks will be skipped when the commit message starts with 'WIP' + skipHookPrefixes: + - WIP # If true, periodically fetch from remote autoFetch: true @@ -1184,3 +1185,94 @@ func TestPagerMigration(t *testing.T) { }) } } + +func TestSkipHookPrefixMigration(t *testing.T) { + scenarios := []struct { + name string + input string + expected string + expectedDidChange bool + expectedChanges []string + }{ + { + name: "Empty Input", + input: "", + expectedDidChange: false, + expectedChanges: []string{}, + }, + { + name: "Empty string", + input: `git: + skipHookPrefix: "" +`, + expected: `git: + skipHookPrefixes: [] +`, + expectedDidChange: true, + expectedChanges: []string{ + "Moved git.skipHookPrefix string to git.skipHookPrefixes array", + }, + }, + { + name: "Old skipHookPrefix migrated to skipHookPrefixes", + input: `git: + skipHookPrefix: WIP +`, + expected: `git: + skipHookPrefixes: + - WIP +`, + expectedDidChange: true, + expectedChanges: []string{ + "Moved git.skipHookPrefix string to git.skipHookPrefixes array", + }, + }, + { + name: "skipHookPrefix is not a string: do nothing", + input: `git: + skipHookPrefix: 5 +`, + expectedDidChange: false, + expectedChanges: []string{}, + }, + { + name: "New skipHookPrefixes already present: do nothing", + input: `git: + skipHookPrefix: WIP + skipHookPrefixes: + - WIP + - fixup! +`, + expectedDidChange: false, + expectedChanges: []string{}, + }, + { + name: "Only new skipHookPrefixes, no migration needed", + input: `git: + skipHookPrefixes: + - WIP +`, + expectedDidChange: false, + expectedChanges: []string{}, + }, + { + name: "Neither present", + input: "git:", + expectedDidChange: false, + expectedChanges: []string{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + changes := NewChangesSet() + actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes) + assert.NoError(t, err) + assert.Equal(t, s.expectedDidChange, didChange) + if didChange { + assert.Equal(t, s.expected, string(actual)) + } + assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest()) + }) + } +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 29778d63928..0de19eaacfb 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -282,8 +282,8 @@ type GitConfig struct { Merging MergingConfig `yaml:"merging"` // list of branches that are considered 'main' branches, used when displaying commits MainBranches []string `yaml:"mainBranches" jsonschema:"uniqueItems=true"` - // Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks will be skipped when the commit message starts with 'WIP' - SkipHookPrefix string `yaml:"skipHookPrefix"` + // Prefixes to use when skipping hooks. E.g. if set to ['WIP'], then pre-commit hooks will be skipped when the commit message starts with 'WIP'. The first entry in the array will be used for the "Commit changes without pre-commit hook" command. + SkipHookPrefixes []string `yaml:"skipHookPrefixes" jsonschema:"uniqueItems=true"` // If true, periodically fetch from remote AutoFetch bool `yaml:"autoFetch"` // If true, periodically refresh files and submodules @@ -859,7 +859,7 @@ func GetDefaultConfig() *UserConfig { }, LocalBranchSortOrder: "date", RemoteBranchSortOrder: "date", - SkipHookPrefix: "WIP", + SkipHookPrefixes: []string{"WIP"}, MainBranches: []string{"master", "main"}, AutoFetch: true, AutoRefresh: true, diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go index d533e1dea40..c13c6b9d757 100644 --- a/pkg/gui/context/commit_message_context.go +++ b/pkg/gui/context/commit_message_context.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" "github.com/spf13/afero" ) @@ -43,8 +44,8 @@ type CommitMessageViewModel struct { onSwitchToEditor func(string) error // the following two fields are used for the display of the "hooks disabled" subtitle - forceSkipHooks bool - skipHooksPrefix string + forceSkipHooks bool + skipHooksPrefixes []string // The message typed in before cycling through history // We store this separately to 'preservedMessage' because 'preservedMessage' @@ -153,7 +154,7 @@ func (self *CommitMessageContext) SetPanelState( onConfirm func(string, string) error, onSwitchToEditor func(string) error, forceSkipHooks bool, - skipHooksPrefix string, + skipHooksPrefixes []string, ) { self.viewModel.selectedindex = index self.viewModel.preserveMessage = preserveMessage @@ -161,7 +162,7 @@ func (self *CommitMessageContext) SetPanelState( self.viewModel.onConfirm = onConfirm self.viewModel.onSwitchToEditor = onSwitchToEditor self.viewModel.forceSkipHooks = forceSkipHooks - self.viewModel.skipHooksPrefix = skipHooksPrefix + self.viewModel.skipHooksPrefixes = skipHooksPrefixes self.GetView().Title = summaryTitle self.c.Views().CommitDescription.Title = descriptionTitle @@ -175,10 +176,12 @@ func (self *CommitMessageContext) SetPanelState( } func (self *CommitMessageContext) RenderSubtitle() { - skipHookPrefix := self.viewModel.skipHooksPrefix subject := self.c.Views().CommitMessage.TextArea.GetContent() var subtitle string - if self.viewModel.forceSkipHooks || (skipHookPrefix != "" && strings.HasPrefix(subject, skipHookPrefix)) { + hooksSkipped := self.viewModel.forceSkipHooks || lo.SomeBy(self.viewModel.skipHooksPrefixes, func(prefix string) bool { + return strings.HasPrefix(subject, prefix) + }) + if hooksSkipped { subtitle = self.c.Tr.CommitHooksDisabledSubTitle } if self.c.UserConfig().Gui.CommitLength.Show { diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index 47170606488..d76465a7ebb 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -126,8 +126,8 @@ type OpenCommitMessagePanelOpts struct { // the actual behavior; make sure what you are passing in matches that. // Leave unassigned if the concept of skipping hooks doesn't make sense for // what you are doing, e.g. when creating a tag. - ForceSkipHooks bool - SkipHooksPrefix string + ForceSkipHooks bool + SkipHooksPrefixes []string } func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) { @@ -146,7 +146,7 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp onConfirm, opts.OnSwitchToEditor, opts.ForceSkipHooks, - opts.SkipHooksPrefix, + opts.SkipHooksPrefixes, ) self.UpdateCommitPanelView(opts.InitialMessage) diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index d6289537bde..5e2e1ad72f0 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -135,8 +135,8 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin OnSwitchToEditor: func(filepath string) error { return self.switchFromCommitMessagePanelToEditor(filepath, forceSkipHooks) }, - ForceSkipHooks: forceSkipHooks, - SkipHooksPrefix: self.c.UserConfig().Git.SkipHookPrefix, + ForceSkipHooks: forceSkipHooks, + SkipHooksPrefixes: self.c.UserConfig().Git.SkipHookPrefixes, }, ) @@ -188,8 +188,11 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error { var initialMessage string preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError() if preservedMessage == "" { - // Use the skipHook prefix only if we don't have a preserved message - initialMessage = self.c.UserConfig().Git.SkipHookPrefix + // Use the first skipHook prefix only if we don't have a preserved message + prefixes := self.c.UserConfig().Git.SkipHookPrefixes + if len(prefixes) > 0 { + initialMessage = prefixes[0] + } } return self.HandleCommitPressWithMessage(initialMessage, true) } diff --git a/schema-master/config.json b/schema-master/config.json index c4312981fa2..045d9256d9f 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -323,10 +323,16 @@ "main" ] }, - "skipHookPrefix": { - "type": "string", - "description": "Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks will be skipped when the commit message starts with 'WIP'", - "default": "WIP" + "skipHookPrefixes": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true, + "description": "Prefixes to use when skipping hooks. E.g. if set to ['WIP'], then pre-commit hooks will be skipped when the commit message starts with 'WIP'. The first entry in the array will be used for the \"Commit changes without pre-commit hook\" command.", + "default": [ + "WIP" + ] }, "autoFetch": { "type": "boolean", From 01f27f5089fea561b767db8e16e567b7c728e1d5 Mon Sep 17 00:00:00 2001 From: Alexander Slavschik Date: Wed, 8 Apr 2026 22:03:32 +0200 Subject: [PATCH 2/2] refactor(commit): adjust skip hook prefix logic for amend and fixup commits --- pkg/commands/git_commands/commit.go | 6 +- pkg/commands/git_commands/commit_test.go | 55 ++++++++++--------- pkg/commands/git_commands/rebase.go | 2 +- .../controllers/local_commits_controller.go | 2 +- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 1c575f6692d..801a1ecbe8b 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -292,9 +292,9 @@ func (self *CommitCommands) Revert(hashes []string, isMerge bool) error { } // CreateFixupCommit creates a commit that fixes up a previous commit -func (self *CommitCommands) CreateFixupCommit(hash string) error { +func (self *CommitCommands) CreateFixupCommit(hash string, originalSubject string) error { cmdArgs := NewGitCmd("commit"). - ArgIf(self.hasSkipHookPrefix("fixup! "), "--no-verify"). + ArgIf(self.hasSkipHookPrefix("fixup! "+originalSubject), "--no-verify"). Arg("--fixup=" + hash). ToArgv() @@ -308,7 +308,7 @@ func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDe description += "\n\n" + newDescription } cmdArgs := NewGitCmd("commit"). - ArgIf(self.hasSkipHookPrefix("amend! "), "--no-verify"). + ArgIf(self.hasSkipHookPrefix(newSubject), "--no-verify"). Arg("-m", "amend! "+originalSubject). Arg("-m", description). ArgIf(!includeFileChanges, "--only", "--allow-empty"). diff --git a/pkg/commands/git_commands/commit_test.go b/pkg/commands/git_commands/commit_test.go index 67d2b8de3ae..97ef91e8657 100644 --- a/pkg/commands/git_commands/commit_test.go +++ b/pkg/commands/git_commands/commit_test.go @@ -179,17 +179,19 @@ func TestCommitCommitEditorCmdObj(t *testing.T) { func TestCommitCreateFixupCommit(t *testing.T) { type scenario struct { - testName string - hash string - userConfig *config.UserConfig - runner *oscommands.FakeCmdObjRunner - test func(error) + testName string + hash string + originalSubject string + userConfig *config.UserConfig + runner *oscommands.FakeCmdObjRunner + test func(error) } scenarios := []scenario{ { - testName: "valid case", - hash: "12345", + testName: "valid case", + hash: "12345", + originalSubject: "some commit", runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"commit", "--fixup=12345"}, "", nil), test: func(err error) { @@ -197,10 +199,11 @@ func TestCommitCreateFixupCommit(t *testing.T) { }, }, { - testName: "with matching skipHookPrefixes", - hash: "12345", + testName: "with matching skipHookPrefixes for original subject", + hash: "12345", + originalSubject: "WIP do stuff", userConfig: &config.UserConfig{ - Git: config.GitConfig{SkipHookPrefixes: []string{"fixup!"}}, + Git: config.GitConfig{SkipHookPrefixes: []string{"fixup! WIP"}}, }, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"commit", "--no-verify", "--fixup=12345"}, "", nil), @@ -209,8 +212,9 @@ func TestCommitCreateFixupCommit(t *testing.T) { }, }, { - testName: "with non-matching skipHookPrefixes", - hash: "12345", + testName: "with non-matching skipHookPrefixes", + hash: "12345", + originalSubject: "some commit", userConfig: &config.UserConfig{ Git: config.GitConfig{SkipHookPrefixes: []string{"WIP"}}, }, @@ -221,10 +225,11 @@ func TestCommitCreateFixupCommit(t *testing.T) { }, }, { - testName: "with multiple prefixes including fixup!", - hash: "12345", + testName: "with multiple prefixes including fixup! WIP", + hash: "12345", + originalSubject: "WIP my feature", userConfig: &config.UserConfig{ - Git: config.GitConfig{SkipHookPrefixes: []string{"WIP", "fixup!"}}, + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP", "fixup! WIP"}}, }, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"commit", "--no-verify", "--fixup=12345"}, "", nil), @@ -237,7 +242,7 @@ func TestCommitCreateFixupCommit(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { instance := buildCommitCommands(commonDeps{runner: s.runner, userConfig: s.userConfig}) - s.test(instance.CreateFixupCommit(s.hash)) + s.test(instance.CreateFixupCommit(s.hash, s.originalSubject)) s.runner.CheckForMissingCalls() }) } @@ -283,16 +288,16 @@ func TestCommitCreateAmendCommit(t *testing.T) { ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject", "--only", "--allow-empty"}, "", nil), }, { - testName: "with matching skipHookPrefixes", + testName: "with matching skipHookPrefixes on new subject", originalSubject: "original subject", - newSubject: "new subject", + newSubject: "WIP new subject", newDescription: "", includeFileChanges: true, userConfig: &config.UserConfig{ - Git: config.GitConfig{SkipHookPrefixes: []string{"amend!"}}, + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP"}}, }, runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"commit", "--no-verify", "-m", "amend! original subject", "-m", "new subject"}, "", nil), + ExpectGitArgs([]string{"commit", "--no-verify", "-m", "amend! original subject", "-m", "WIP new subject"}, "", nil), }, { testName: "with non-matching skipHookPrefixes", @@ -307,16 +312,16 @@ func TestCommitCreateAmendCommit(t *testing.T) { ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject"}, "", nil), }, { - testName: "with multiple prefixes including amend!", - originalSubject: "original subject", - newSubject: "new subject", + testName: "renaming WIP commit to non-WIP runs hooks", + originalSubject: "WIP my feature", + newSubject: "Implement my feature", newDescription: "", includeFileChanges: true, userConfig: &config.UserConfig{ - Git: config.GitConfig{SkipHookPrefixes: []string{"WIP", "amend!"}}, + Git: config.GitConfig{SkipHookPrefixes: []string{"WIP"}}, }, runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"commit", "--no-verify", "-m", "amend! original subject", "-m", "new subject"}, "", nil), + ExpectGitArgs([]string{"commit", "-m", "amend! WIP my feature", "-m", "Implement my feature"}, "", nil), }, } diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index 97d48a1a01f..12ff9c9ba33 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -298,7 +298,7 @@ func (self *RebaseCommands) getHashOfLastCommitMade() (string, error) { func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error { commit := commits[commitIndex] - if err := self.commit.CreateFixupCommit(commit.Hash()); err != nil { + if err := self.commit.CreateFixupCommit(commit.Hash(), commit.Name); err != nil { return err } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index e4f88b3ca68..4e772da6c5f 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -1006,7 +1006,7 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err return self.c.Helpers().WorkingTree.WithEnsureCommittableFiles(func() error { self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit) return self.c.WithWaitingStatusSync(self.c.Tr.CreatingFixupCommitStatus, func() error { - if err := self.c.Git().Commit.CreateFixupCommit(commit.Hash()); err != nil { + if err := self.c.Git().Commit.CreateFixupCommit(commit.Hash(), commit.Name); err != nil { return err }