Repository: jesseduffield/lazygit (language: go)

Issue:
SelectedSubCommit is nil in the commits tab
**Describe the bug**
Title

**To Reproduce**
1. Add a custom command like 
```
  - key: O
    context: commitFiles
    description: open files in commit
    command: $EDITOR -c 'Gedit {{.SelectedSubCommit.Sha}}:{{.SelectedCommitFile.Name}}'
    subprocess: true
```
2. Go to commits tab
3. Click on any commit
4. Scroll down to a file
5. Click the O key
6. See error

**Expected behavior**
SelectedSubCommit should not be null

**Screenshots**
![image_2024-06-14_22-19-28](https://github.com/jesseduffield/lazygit/assets/29950594/31e184cb-287a-4a34-b772-2bbe1e17c3b1)


**Version info:**
commit=v0.42.0, build date=2024-05-19T10:54:29Z, build source=binaryRelease, version=0.42.0, os=linux, arch=amd64, git version=2.45.1



Discussion / hints:
`SelectedSubCommit` is for when you hit enter on a branch or tag to look at its commits. If you use your custom command on the commit files of one of those, it will work. For normal commits, you want `SelectedLocalCommit`.

Now, it is a problem that the `commitFiles` context is used for all three cases (local commits, reflog commits, and subcommits), so you would have to use different template variables in each of these situations, but it's impossible to make the distinction.

I'm actually wondering why we need different variables `SelectedLocalCommit`, `SelectedReflogCommit`, and `SelectedSubCommit`. Wouldn't it be better to only have a single `SelectedCommit` variable, and decide based on context what model object to set it to? Am I missing something @jesseduffield?
I agree. It would be good to do the same with SelectedPath (which currently only refers to the files panel and does not include the commit files panel). 

Of course for backwards compatibility we'd want to keep the existing keys (though I think it's fine to repurpose SelectedFile and SelectedPath without introducing new keys for those)

Fix patch:
diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md
index dd7c11af78d..432625693ec 100644
--- a/docs/Custom_Command_Keybindings.md
+++ b/docs/Custom_Command_Keybindings.md
@@ -296,9 +296,7 @@ Here's an example using a command but not specifying anything else: so each line
 Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/golang/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
 
 ```
-SelectedLocalCommit
-SelectedReflogCommit
-SelectedSubCommit
+SelectedCommit
 SelectedFile
 SelectedPath
 SelectedLocalBranch
@@ -311,6 +309,9 @@ SelectedWorktree
 CheckedOutBranch
 ```
 
+(For legacy reasons, `SelectedLocalCommit`, `SelectedReflogCommit`, and `SelectedSubCommit` are also available, but they are deprecated.)
+
+
 To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/models.go) (all the modelling lives in the same file).
 
 ## Keybinding collisions
diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index abb7cb6eebe..3ac1425fbdb 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -300,6 +300,18 @@ func (self *ContextMgr) IsCurrent(c types.Context) bool {
 	return self.Current().GetKey() == c.GetKey()
 }
 
+func (self *ContextMgr) IsCurrentOrParent(c types.Context) bool {
+	current := self.Current()
+	for current != nil {
+		if current.GetKey() == c.GetKey() {
+			return true
+		}
+		current = current.GetParentContext()
+	}
+
+	return false
+}
+
 func (self *ContextMgr) AllFilterable() []types.IFilterableContext {
 	var result []types.IFilterableContext
 
diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go
index 6f39c5f8cd2..cbd04bb14ad 100644
--- a/pkg/gui/services/custom_commands/session_state_loader.go
+++ b/pkg/gui/services/custom_commands/session_state_loader.go
@@ -164,9 +164,10 @@ func worktreeShimFromModelRemote(worktree *models.Worktree) *Worktree {
 
 // SessionState captures the current state of the application for use in custom commands
 type SessionState struct {
-	SelectedLocalCommit    *Commit
-	SelectedReflogCommit   *Commit
-	SelectedSubCommit      *Commit
+	SelectedLocalCommit    *Commit // deprecated, use SelectedCommit
+	SelectedReflogCommit   *Commit // deprecated, use SelectedCommit
+	SelectedSubCommit      *Commit // deprecated, use SelectedCommit
+	SelectedCommit         *Commit
 	SelectedFile           *File
 	SelectedPath           string
 	SelectedLocalBranch    *Branch
@@ -181,19 +182,38 @@ type SessionState struct {
 }
 
 func (self *SessionStateLoader) call() *SessionState {
+	selectedLocalCommit := commitShimFromModelCommit(self.c.Contexts().LocalCommits.GetSelected())
+	selectedReflogCommit := commitShimFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected())
+	selectedSubCommit := commitShimFromModelCommit(self.c.Contexts().SubCommits.GetSelected())
+
+	selectedCommit := selectedLocalCommit
+	if self.c.Context().IsCurrentOrParent(self.c.Contexts().ReflogCommits) {
+		selectedCommit = selectedReflogCommit
+	} else if self.c.Context().IsCurrentOrParent(self.c.Contexts().SubCommits) {
+		selectedCommit = selectedSubCommit
+	}
+
+	selectedPath := self.c.Contexts().Files.GetSelectedPath()
+	selectedCommitFilePath := self.c.Contexts().CommitFiles.GetSelectedPath()
+
+	if self.c.Context().IsCurrent(self.c.Contexts().CommitFiles) {
+		selectedPath = selectedCommitFilePath
+	}
+
 	return &SessionState{
 		SelectedFile:           fileShimFromModelFile(self.c.Contexts().Files.GetSelectedFile()),
-		SelectedPath:           self.c.Contexts().Files.GetSelectedPath(),
-		SelectedLocalCommit:    commitShimFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()),
-		SelectedReflogCommit:   commitShimFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()),
+		SelectedPath:           selectedPath,
+		SelectedLocalCommit:    selectedLocalCommit,
+		SelectedReflogCommit:   selectedReflogCommit,
+		SelectedSubCommit:      selectedSubCommit,
+		SelectedCommit:         selectedCommit,
 		SelectedLocalBranch:    branchShimFromModelBranch(self.c.Contexts().Branches.GetSelected()),
 		SelectedRemoteBranch:   remoteBranchShimFromModelRemoteBranch(self.c.Contexts().RemoteBranches.GetSelected()),
 		SelectedRemote:         remoteShimFromModelRemote(self.c.Contexts().Remotes.GetSelected()),
 		SelectedTag:            tagShimFromModelRemote(self.c.Contexts().Tags.GetSelected()),
 		SelectedStashEntry:     stashEntryShimFromModelRemote(self.c.Contexts().Stash.GetSelected()),
 		SelectedCommitFile:     commitFileShimFromModelRemote(self.c.Contexts().CommitFiles.GetSelectedFile()),
-		SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(),
-		SelectedSubCommit:      commitShimFromModelCommit(self.c.Contexts().SubCommits.GetSelected()),
+		SelectedCommitFilePath: selectedCommitFilePath,
 		SelectedWorktree:       worktreeShimFromModelRemote(self.c.Contexts().Worktrees.GetSelected()),
 		CheckedOutBranch:       branchShimFromModelBranch(self.refsHelper.GetCheckedOutRef()),
 	}
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index 49a6e6e356e..2948f2eda5e 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -284,6 +284,7 @@ type IContextMgr interface {
 	CurrentSide() Context
 	CurrentPopup() []Context
 	IsCurrent(c Context) bool
+	IsCurrentOrParent(c Context) bool
 	ForEach(func(Context))
 	AllList() []IListContext
 	AllFilterable() []IFilterableContext


Test patch:
diff --git a/pkg/integration/tests/custom_commands/selected_commit.go b/pkg/integration/tests/custom_commands/selected_commit.go
new file mode 100644
index 00000000000..0dda1954604
--- /dev/null
+++ b/pkg/integration/tests/custom_commands/selected_commit.go
@@ -0,0 +1,67 @@
+package custom_commands
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SelectedCommit = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Use the {{ .SelectedCommit }} template variable in different contexts",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupRepo: func(shell *Shell) {
+		shell.CreateNCommits(3)
+	},
+	SetupConfig: func(cfg *config.AppConfig) {
+		cfg.UserConfig.CustomCommands = []config.CustomCommand{
+			{
+				Key:     "X",
+				Context: "global",
+				Command: "printf '%s' '{{ .SelectedCommit.Name }}' > file.txt",
+			},
+		}
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		// Select different commits in each of the commit views
+		t.Views().Commits().Focus().
+			NavigateToLine(Contains("commit 01"))
+		t.Views().ReflogCommits().Focus().
+			NavigateToLine(Contains("commit 02"))
+		t.Views().Branches().Focus().
+			Lines(Contains("master").IsSelected()).
+			PressEnter()
+		t.Views().SubCommits().IsFocused().
+			NavigateToLine(Contains("commit 03"))
+
+		// SubCommits
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 03"))
+
+		t.Views().SubCommits().PressEnter()
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 03"))
+
+		// ReflogCommits
+		t.Views().ReflogCommits().Focus()
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit: commit 02"))
+
+		t.Views().ReflogCommits().PressEnter()
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit: commit 02"))
+
+		// LocalCommits
+		t.Views().Commits().Focus()
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 01"))
+
+		t.Views().Commits().PressEnter()
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 01"))
+
+		// None of these
+		t.Views().Files().Focus()
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 01"))
+	},
+})
diff --git a/pkg/integration/tests/custom_commands/selected_path.go b/pkg/integration/tests/custom_commands/selected_path.go
new file mode 100644
index 00000000000..f76fb3fbaf2
--- /dev/null
+++ b/pkg/integration/tests/custom_commands/selected_path.go
@@ -0,0 +1,44 @@
+package custom_commands
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SelectedPath = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Use the {{ .SelectedPath }} template variable in different contexts",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupRepo: func(shell *Shell) {
+		shell.CreateDir("folder1")
+		shell.CreateFileAndAdd("folder1/file1", "")
+		shell.Commit("commit")
+		shell.CreateDir("folder2")
+		shell.CreateFile("folder2/file2", "")
+	},
+	SetupConfig: func(cfg *config.AppConfig) {
+		cfg.UserConfig.CustomCommands = []config.CustomCommand{
+			{
+				Key:     "X",
+				Context: "global",
+				Command: "printf '%s' '{{ .SelectedPath }}' > file.txt",
+			},
+		}
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Files().
+			Focus().
+			NavigateToLine(Contains("file2"))
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("folder2/file2"))
+
+		t.Views().Commits().
+			Focus().
+			PressEnter()
+		t.Views().CommitFiles().
+			IsFocused().
+			NavigateToLine(Contains("file1"))
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("folder1/file1"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index c1038ee2a0a..fc126a94934 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -124,6 +124,8 @@ var tests = []*components.IntegrationTest{
 	custom_commands.MenuFromCommandsOutput,
 	custom_commands.MultipleContexts,
 	custom_commands.MultiplePrompts,
+	custom_commands.SelectedCommit,
+	custom_commands.SelectedPath,
 	custom_commands.ShowOutputInPanel,
 	custom_commands.SuggestionsCommand,
 	custom_commands.SuggestionsPreset,