Repository: jesseduffield/lazygit (language: go)

Issue:
Expose selected commit ranges to custom commands
**Is your feature request related to a problem? Please describe.**

Currently we can access a single selected commit via `{{.SelectedCommit.Sha}}`.
However using the `v` key it's also possible to select multiple commits.
I.e. this allows to create a pull request from a range of commits or doing other types of automation that require access to multiple commits.

**Describe the solution you'd like**

A potential API could look like thi
s:
`{{.SelectedCommits.StartSha}}` `{{.SelectedCommits.EndSha}}`

**Describe alternatives you've considered**

Can't think of a different way


From my understanding this needs to extend

https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/session_state_loader.go#L203

The only thing that is unclear to me is what should happen if the user has not selected anything?


Discussion / hints:

Fix patch:
diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md
index 423f5ecefb1..b940a7bb3c4 100644
--- a/docs/Custom_Command_Keybindings.md
+++ b/docs/Custom_Command_Keybindings.md
@@ -297,6 +297,7 @@ Your commands can contain placeholder strings using Go's [template syntax](https
 
 ```
 SelectedCommit
+SelectedCommitRange
 SelectedFile
 SelectedPath
 SelectedLocalBranch
@@ -314,6 +315,11 @@ CheckedOutBranch
 
 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).
 
+We don't support accessing all elements of a range selection yet. We might add this in the future, but as a special case you can access the range of selected commits by using `SelectedCommitRange`, which has two properties `.To` and `.From` which are the hashes of the bottom and top selected commits, respectively. This is useful for passing them to a git command that operates on a range of commits. For example, to create patches for all selected commits, you might use
+```yml
+  command: "git format-patch {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}}"
+```
+
 ## Keybinding collisions
 
 If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go
index cbd04bb14ad..8d6ef5b480b 100644
--- a/pkg/gui/services/custom_commands/session_state_loader.go
+++ b/pkg/gui/services/custom_commands/session_state_loader.go
@@ -162,12 +162,29 @@ func worktreeShimFromModelRemote(worktree *models.Worktree) *Worktree {
 	}
 }
 
+type CommitRange struct {
+	From string
+	To   string
+}
+
+func makeCommitRange(commits []*models.Commit, _ int, _ int) *CommitRange {
+	if len(commits) == 0 {
+		return nil
+	}
+
+	return &CommitRange{
+		From: commits[len(commits)-1].Hash,
+		To:   commits[0].Hash,
+	}
+}
+
 // SessionState captures the current state of the application for use in custom commands
 type SessionState struct {
 	SelectedLocalCommit    *Commit // deprecated, use SelectedCommit
 	SelectedReflogCommit   *Commit // deprecated, use SelectedCommit
 	SelectedSubCommit      *Commit // deprecated, use SelectedCommit
 	SelectedCommit         *Commit
+	SelectedCommitRange    *CommitRange
 	SelectedFile           *File
 	SelectedPath           string
 	SelectedLocalBranch    *Branch
@@ -183,14 +200,20 @@ type SessionState struct {
 
 func (self *SessionStateLoader) call() *SessionState {
 	selectedLocalCommit := commitShimFromModelCommit(self.c.Contexts().LocalCommits.GetSelected())
+	selectedLocalCommitRange := makeCommitRange(self.c.Contexts().LocalCommits.GetSelectedItems())
 	selectedReflogCommit := commitShimFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected())
+	selectedReflogCommitRange := makeCommitRange(self.c.Contexts().ReflogCommits.GetSelectedItems())
 	selectedSubCommit := commitShimFromModelCommit(self.c.Contexts().SubCommits.GetSelected())
+	selectedSubCommitRange := makeCommitRange(self.c.Contexts().SubCommits.GetSelectedItems())
 
 	selectedCommit := selectedLocalCommit
+	selectedCommitRange := selectedLocalCommitRange
 	if self.c.Context().IsCurrentOrParent(self.c.Contexts().ReflogCommits) {
 		selectedCommit = selectedReflogCommit
+		selectedCommitRange = selectedReflogCommitRange
 	} else if self.c.Context().IsCurrentOrParent(self.c.Contexts().SubCommits) {
 		selectedCommit = selectedSubCommit
+		selectedCommitRange = selectedSubCommitRange
 	}
 
 	selectedPath := self.c.Contexts().Files.GetSelectedPath()
@@ -207,6 +230,7 @@ func (self *SessionStateLoader) call() *SessionState {
 		SelectedReflogCommit:   selectedReflogCommit,
 		SelectedSubCommit:      selectedSubCommit,
 		SelectedCommit:         selectedCommit,
+		SelectedCommitRange:    selectedCommitRange,
 		SelectedLocalBranch:    branchShimFromModelBranch(self.c.Contexts().Branches.GetSelected()),
 		SelectedRemoteBranch:   remoteBranchShimFromModelRemoteBranch(self.c.Contexts().RemoteBranches.GetSelected()),
 		SelectedRemote:         remoteShimFromModelRemote(self.c.Contexts().Remotes.GetSelected()),


Test patch:
diff --git a/pkg/integration/tests/custom_commands/selected_commit_range.go b/pkg/integration/tests/custom_commands/selected_commit_range.go
new file mode 100644
index 00000000000..1a4b3087c1e
--- /dev/null
+++ b/pkg/integration/tests/custom_commands/selected_commit_range.go
@@ -0,0 +1,41 @@
+package custom_commands
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SelectedCommitRange = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Use the {{ .SelectedCommitRange }} template variable",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupRepo: func(shell *Shell) {
+		shell.CreateNCommits(3)
+	},
+	SetupConfig: func(cfg *config.AppConfig) {
+		cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
+			{
+				Key:     "X",
+				Context: "global",
+				Command: `git log --format="%s" {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} > file.txt`,
+			},
+		}
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().Focus().
+			Lines(
+				Contains("commit 03").IsSelected(),
+				Contains("commit 02"),
+				Contains("commit 01"),
+			)
+
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 03\n"))
+
+		t.Views().Commits().Focus().
+			Press(keys.Universal.RangeSelectDown)
+
+		t.GlobalPress("X")
+		t.FileSystem().FileContent("file.txt", Equals("commit 03\ncommit 02\n"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index a6825676af0..0736677a80b 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -139,6 +139,7 @@ var tests = []*components.IntegrationTest{
 	custom_commands.MultipleContexts,
 	custom_commands.MultiplePrompts,
 	custom_commands.SelectedCommit,
+	custom_commands.SelectedCommitRange,
 	custom_commands.SelectedPath,
 	custom_commands.ShowOutputInPanel,
 	custom_commands.SuggestionsCommand,