1package gui
2
3import (
4	"fmt"
5	"sync"
6
7	"github.com/jesseduffield/lazygit/pkg/commands"
8	"github.com/jesseduffield/lazygit/pkg/commands/models"
9	"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
10	"github.com/jesseduffield/lazygit/pkg/utils"
11)
12
13// after selecting the 200th commit, we'll load in all the rest
14const COMMIT_THRESHOLD = 200
15
16// list panel functions
17
18func (gui *Gui) getSelectedLocalCommit() *models.Commit {
19	selectedLine := gui.State.Panels.Commits.SelectedLineIdx
20	if selectedLine == -1 || selectedLine > len(gui.State.Commits)-1 {
21		return nil
22	}
23
24	return gui.State.Commits[selectedLine]
25}
26
27func (gui *Gui) handleCommitSelect() error {
28	state := gui.State.Panels.Commits
29	if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
30		state.LimitCommits = false
31		go utils.Safe(func() {
32			if err := gui.refreshCommitsWithLimit(); err != nil {
33				_ = gui.surfaceError(err)
34			}
35		})
36	}
37
38	gui.escapeLineByLinePanel()
39
40	var task updateTask
41	commit := gui.getSelectedLocalCommit()
42	if commit == nil {
43		task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
44	} else {
45		cmd := gui.OSCommand.ExecutableFromString(
46			gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
47		)
48		task = NewRunPtyTask(cmd)
49	}
50
51	return gui.refreshMainViews(refreshMainOpts{
52		main: &viewUpdateOpts{
53			title: "Patch",
54			task:  task,
55		},
56		secondary: gui.secondaryPatchPanelUpdateOpts(),
57	})
58}
59
60// during startup, the bottleneck is fetching the reflog entries. We need these
61// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
62// In the initial phase we don't get any reflog commits, but we asynchronously get them
63// and refresh the branches after that
64func (gui *Gui) refreshReflogCommitsConsideringStartup() {
65	switch gui.State.StartupStage {
66	case INITIAL:
67		go utils.Safe(func() {
68			_ = gui.refreshReflogCommits()
69			gui.refreshBranches()
70			gui.State.StartupStage = COMPLETE
71		})
72
73	case COMPLETE:
74		_ = gui.refreshReflogCommits()
75	}
76}
77
78// whenever we change commits, we should update branches because the upstream/downstream
79// counts can change. Whenever we change branches we should probably also change commits
80// e.g. in the case of switching branches.
81func (gui *Gui) refreshCommits() error {
82	wg := sync.WaitGroup{}
83	wg.Add(2)
84
85	go utils.Safe(func() {
86		gui.refreshReflogCommitsConsideringStartup()
87
88		gui.refreshBranches()
89		wg.Done()
90	})
91
92	go utils.Safe(func() {
93		_ = gui.refreshCommitsWithLimit()
94		context, ok := gui.State.Contexts.CommitFiles.GetParentContext()
95		if ok && context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
96			// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
97			// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
98			// showing the contents of a different commit than the one we initially entered.
99			// Ideally we would know when to refresh the commit files context and when not to,
100			// or perhaps we could just pop that context off the stack whenever cycling windows.
101			// For now the awkwardness remains.
102			commit := gui.getSelectedLocalCommit()
103			if commit != nil {
104				gui.State.Panels.CommitFiles.refName = commit.RefName()
105				_ = gui.refreshCommitFilesView()
106			}
107		}
108		wg.Done()
109	})
110
111	wg.Wait()
112
113	return nil
114}
115
116func (gui *Gui) refreshCommitsWithLimit() error {
117	gui.Mutexes.BranchCommitsMutex.Lock()
118	defer gui.Mutexes.BranchCommitsMutex.Unlock()
119
120	builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
121
122	commits, err := builder.GetCommits(
123		commands.GetCommitsOptions{
124			Limit:                gui.State.Panels.Commits.LimitCommits,
125			FilterPath:           gui.State.Modes.Filtering.GetPath(),
126			IncludeRebaseCommits: true,
127			RefName:              "HEAD",
128			All:                  gui.State.ShowWholeGitGraph,
129		},
130	)
131	if err != nil {
132		return err
133	}
134	gui.State.Commits = commits
135
136	return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
137}
138
139func (gui *Gui) refreshRebaseCommits() error {
140	gui.Mutexes.BranchCommitsMutex.Lock()
141	defer gui.Mutexes.BranchCommitsMutex.Unlock()
142
143	builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
144
145	updatedCommits, err := builder.MergeRebasingCommits(gui.State.Commits)
146	if err != nil {
147		return err
148	}
149	gui.State.Commits = updatedCommits
150
151	return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
152}
153
154// specific functions
155
156func (gui *Gui) handleCommitSquashDown() error {
157	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
158		return err
159	}
160
161	if len(gui.State.Commits) <= 1 {
162		return gui.createErrorPanel(gui.Tr.YouNoCommitsToSquash)
163	}
164
165	applied, err := gui.handleMidRebaseCommand("squash")
166	if err != nil {
167		return err
168	}
169	if applied {
170		return nil
171	}
172
173	return gui.ask(askOpts{
174		title:  gui.Tr.Squash,
175		prompt: gui.Tr.SureSquashThisCommit,
176		handleConfirm: func() error {
177			return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
178				err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashCommitDown).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
179				return gui.handleGenericMergeCommandResult(err)
180			})
181		},
182	})
183}
184
185func (gui *Gui) handleCommitFixup() error {
186	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
187		return err
188	}
189
190	if len(gui.State.Commits) <= 1 {
191		return gui.createErrorPanel(gui.Tr.YouNoCommitsToSquash)
192	}
193
194	applied, err := gui.handleMidRebaseCommand("fixup")
195	if err != nil {
196		return err
197	}
198	if applied {
199		return nil
200	}
201
202	return gui.ask(askOpts{
203		title:  gui.Tr.Fixup,
204		prompt: gui.Tr.SureFixupThisCommit,
205		handleConfirm: func() error {
206			return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
207				err := gui.GitCommand.WithSpan(gui.Tr.Spans.FixupCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
208				return gui.handleGenericMergeCommandResult(err)
209			})
210		},
211	})
212}
213
214func (gui *Gui) handleRenameCommit() error {
215	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
216		return err
217	}
218
219	applied, err := gui.handleMidRebaseCommand("reword")
220	if err != nil {
221		return err
222	}
223	if applied {
224		return nil
225	}
226
227	if gui.State.Panels.Commits.SelectedLineIdx != 0 {
228		return gui.createErrorPanel(gui.Tr.OnlyRenameTopCommit)
229	}
230
231	commit := gui.getSelectedLocalCommit()
232	if commit == nil {
233		return nil
234	}
235
236	message, err := gui.GitCommand.GetCommitMessage(commit.Sha)
237	if err != nil {
238		return gui.surfaceError(err)
239	}
240
241	return gui.prompt(promptOpts{
242		title:          gui.Tr.LcRenameCommit,
243		initialContent: message,
244		handleConfirm: func(response string) error {
245			if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RenameCommit(response); err != nil {
246				return gui.surfaceError(err)
247			}
248
249			return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
250		},
251	})
252}
253
254func (gui *Gui) handleRenameCommitEditor() error {
255	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
256		return err
257	}
258
259	applied, err := gui.handleMidRebaseCommand("reword")
260	if err != nil {
261		return err
262	}
263	if applied {
264		return nil
265	}
266
267	subProcess, err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
268	if err != nil {
269		return gui.surfaceError(err)
270	}
271	if subProcess != nil {
272		return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
273	}
274
275	return nil
276}
277
278// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
279// commit meaning you are trying to edit the todo file rather than actually
280// begin a rebase. It then updates the todo file with that action
281func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
282	selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx]
283	if selectedCommit.Status != "rebasing" {
284		return false, nil
285	}
286
287	// for now we do not support setting 'reword' because it requires an editor
288	// and that means we either unconditionally wait around for the subprocess to ask for
289	// our input or we set a lazygit client as the EDITOR env variable and have it
290	// request us to edit the commit message when prompted.
291	if action == "reword" {
292		return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported)
293	}
294
295	gui.OnRunCommand(oscommands.NewCmdLogEntry(
296		fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
297		"Update rebase TODO",
298		false,
299	))
300
301	if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
302		return false, gui.surfaceError(err)
303	}
304
305	return true, gui.refreshRebaseCommits()
306}
307
308func (gui *Gui) handleCommitDelete() error {
309	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
310		return err
311	}
312
313	applied, err := gui.handleMidRebaseCommand("drop")
314	if err != nil {
315		return err
316	}
317	if applied {
318		return nil
319	}
320
321	return gui.ask(askOpts{
322		title:  gui.Tr.DeleteCommitTitle,
323		prompt: gui.Tr.DeleteCommitPrompt,
324		handleConfirm: func() error {
325			return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
326				err := gui.GitCommand.WithSpan(gui.Tr.Spans.DropCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
327				return gui.handleGenericMergeCommandResult(err)
328			})
329		},
330	})
331}
332
333func (gui *Gui) handleCommitMoveDown() error {
334	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
335		return err
336	}
337
338	span := gui.Tr.Spans.MoveCommitDown
339
340	index := gui.State.Panels.Commits.SelectedLineIdx
341	selectedCommit := gui.State.Commits[index]
342	if selectedCommit.Status == "rebasing" {
343		if gui.State.Commits[index+1].Status != "rebasing" {
344			return nil
345		}
346
347		// logging directly here because MoveTodoDown doesn't have enough information
348		// to provide a useful log
349		gui.OnRunCommand(oscommands.NewCmdLogEntry(
350			fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()),
351			span,
352			false,
353		))
354
355		if err := gui.GitCommand.MoveTodoDown(index); err != nil {
356			return gui.surfaceError(err)
357		}
358		gui.State.Panels.Commits.SelectedLineIdx++
359		return gui.refreshRebaseCommits()
360	}
361
362	return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
363		err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index)
364		if err == nil {
365			gui.State.Panels.Commits.SelectedLineIdx++
366		}
367		return gui.handleGenericMergeCommandResult(err)
368	})
369}
370
371func (gui *Gui) handleCommitMoveUp() error {
372	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
373		return err
374	}
375
376	index := gui.State.Panels.Commits.SelectedLineIdx
377	if index == 0 {
378		return nil
379	}
380
381	span := gui.Tr.Spans.MoveCommitUp
382
383	selectedCommit := gui.State.Commits[index]
384	if selectedCommit.Status == "rebasing" {
385		// logging directly here because MoveTodoDown doesn't have enough information
386		// to provide a useful log
387		gui.OnRunCommand(oscommands.NewCmdLogEntry(
388			fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
389			span,
390			false,
391		))
392
393		if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
394			return gui.surfaceError(err)
395		}
396		gui.State.Panels.Commits.SelectedLineIdx--
397		return gui.refreshRebaseCommits()
398	}
399
400	return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
401		err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index-1)
402		if err == nil {
403			gui.State.Panels.Commits.SelectedLineIdx--
404		}
405		return gui.handleGenericMergeCommandResult(err)
406	})
407}
408
409func (gui *Gui) handleCommitEdit() error {
410	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
411		return err
412	}
413
414	applied, err := gui.handleMidRebaseCommand("edit")
415	if err != nil {
416		return err
417	}
418	if applied {
419		return nil
420	}
421
422	return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
423		err = gui.GitCommand.WithSpan(gui.Tr.Spans.EditCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
424		return gui.handleGenericMergeCommandResult(err)
425	})
426}
427
428func (gui *Gui) handleCommitAmendTo() error {
429	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
430		return err
431	}
432
433	return gui.ask(askOpts{
434		title:  gui.Tr.AmendCommitTitle,
435		prompt: gui.Tr.AmendCommitPrompt,
436		handleConfirm: func() error {
437			return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
438				err := gui.GitCommand.WithSpan(gui.Tr.Spans.AmendCommit).AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
439				return gui.handleGenericMergeCommandResult(err)
440			})
441		},
442	})
443}
444
445func (gui *Gui) handleCommitPick() error {
446	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
447		return err
448	}
449
450	applied, err := gui.handleMidRebaseCommand("pick")
451	if err != nil {
452		return err
453	}
454	if applied {
455		return nil
456	}
457
458	// at this point we aren't actually rebasing so we will interpret this as an
459	// attempt to pull. We might revoke this later after enabling configurable keybindings
460	return gui.handlePullFiles()
461}
462
463func (gui *Gui) handleCommitRevert() error {
464	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
465		return err
466	}
467
468	commit := gui.getSelectedLocalCommit()
469
470	if commit.IsMerge() {
471		return gui.createRevertMergeCommitMenu(commit)
472	} else {
473		if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(commit.Sha); err != nil {
474			return gui.surfaceError(err)
475		}
476		return gui.afterRevertCommit()
477	}
478}
479
480func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
481	menuItems := make([]*menuItem, len(commit.Parents))
482	for i, parentSha := range commit.Parents {
483		i := i
484		message, err := gui.GitCommand.GetCommitMessageFirstLine(parentSha)
485		if err != nil {
486			return gui.surfaceError(err)
487		}
488
489		menuItems[i] = &menuItem{
490			displayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
491			onPress: func() error {
492				parentNumber := i + 1
493				if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).RevertMerge(commit.Sha, parentNumber); err != nil {
494					return gui.surfaceError(err)
495				}
496				return gui.afterRevertCommit()
497			},
498		}
499	}
500
501	return gui.createMenu(gui.Tr.SelectParentCommitForMerge, menuItems, createMenuOptions{showCancel: true})
502}
503
504func (gui *Gui) afterRevertCommit() error {
505	gui.State.Panels.Commits.SelectedLineIdx++
506	return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}})
507}
508
509func (gui *Gui) handleViewCommitFiles() error {
510	commit := gui.getSelectedLocalCommit()
511	if commit == nil {
512		return nil
513	}
514
515	return gui.switchToCommitFilesContext(commit.Sha, true, gui.State.Contexts.BranchCommits, "commits")
516}
517
518func (gui *Gui) handleCreateFixupCommit() error {
519	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
520		return err
521	}
522
523	commit := gui.getSelectedLocalCommit()
524	if commit == nil {
525		return nil
526	}
527
528	prompt := utils.ResolvePlaceholderString(
529		gui.Tr.SureCreateFixupCommit,
530		map[string]string{
531			"commit": commit.Sha,
532		},
533	)
534
535	return gui.ask(askOpts{
536		title:  gui.Tr.CreateFixupCommit,
537		prompt: prompt,
538		handleConfirm: func() error {
539			if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateFixupCommit).CreateFixupCommit(commit.Sha); err != nil {
540				return gui.surfaceError(err)
541			}
542
543			return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
544		},
545	})
546}
547
548func (gui *Gui) handleSquashAllAboveFixupCommits() error {
549	if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
550		return err
551	}
552
553	commit := gui.getSelectedLocalCommit()
554	if commit == nil {
555		return nil
556	}
557
558	prompt := utils.ResolvePlaceholderString(
559		gui.Tr.SureSquashAboveCommits,
560		map[string]string{
561			"commit": commit.Sha,
562		},
563	)
564
565	return gui.ask(askOpts{
566		title:  gui.Tr.SquashAboveCommits,
567		prompt: prompt,
568		handleConfirm: func() error {
569			return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
570				err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashAllAboveFixupCommits).SquashAllAboveFixupCommits(commit.Sha)
571				return gui.handleGenericMergeCommandResult(err)
572			})
573		},
574	})
575}
576
577func (gui *Gui) handleTagCommit() error {
578	// TODO: bring up menu asking if you want to make a lightweight or annotated tag
579	// if annotated, switch to a subprocess to create the message
580
581	commit := gui.getSelectedLocalCommit()
582	if commit == nil {
583		return nil
584	}
585
586	return gui.handleCreateLightweightTag(commit.Sha)
587}
588
589func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
590	return gui.prompt(promptOpts{
591		title: gui.Tr.TagNameTitle,
592		handleConfirm: func(response string) error {
593			if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(response, commitSha); err != nil {
594				return gui.surfaceError(err)
595			}
596			return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
597		},
598	})
599}
600
601func (gui *Gui) handleCheckoutCommit() error {
602	commit := gui.getSelectedLocalCommit()
603	if commit == nil {
604		return nil
605	}
606
607	return gui.ask(askOpts{
608		title:  gui.Tr.LcCheckoutCommit,
609		prompt: gui.Tr.SureCheckoutThisCommit,
610		handleConfirm: func() error {
611			return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutCommit})
612		},
613	})
614}
615
616func (gui *Gui) handleCreateCommitResetMenu() error {
617	commit := gui.getSelectedLocalCommit()
618	if commit == nil {
619		return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
620	}
621
622	return gui.createResetMenu(commit.Sha)
623}
624
625func (gui *Gui) handleOpenSearchForCommitsPanel(_viewName string) error {
626	// we usually lazyload these commits but now that we're searching we need to load them now
627	if gui.State.Panels.Commits.LimitCommits {
628		gui.State.Panels.Commits.LimitCommits = false
629		if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS}}); err != nil {
630			return err
631		}
632	}
633
634	return gui.handleOpenSearch("commits")
635}
636
637func (gui *Gui) handleGotoBottomForCommitsPanel() error {
638	// we usually lazyload these commits but now that we're searching we need to load them now
639	if gui.State.Panels.Commits.LimitCommits {
640		gui.State.Panels.Commits.LimitCommits = false
641		if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}); err != nil {
642			return err
643		}
644	}
645
646	for _, context := range gui.getListContexts() {
647		if context.GetViewName() == "commits" {
648			return context.handleGotoBottom()
649		}
650	}
651
652	return nil
653}
654
655func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
656	commit := gui.getSelectedLocalCommit()
657	if commit == nil {
658		return nil
659	}
660
661	message, err := gui.GitCommand.GetCommitMessage(commit.Sha)
662	if err != nil {
663		return gui.surfaceError(err)
664	}
665
666	if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CopyCommitMessageToClipboard).CopyToClipboard(message); err != nil {
667		return gui.surfaceError(err)
668	}
669
670	gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard)
671
672	return nil
673}
674
675func (gui *Gui) handleOpenLogMenu() error {
676	return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
677		{
678			displayString: gui.Tr.ToggleShowGitGraphAll,
679			onPress: func() error {
680				gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
681
682				if gui.State.ShowWholeGitGraph {
683					gui.State.Panels.Commits.LimitCommits = false
684				}
685
686				return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
687					return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
688				})
689			},
690		},
691		{
692			displayString: gui.Tr.ShowGitGraph,
693			opensMenu:     true,
694			onPress: func() error {
695				onSelect := func(value string) {
696					gui.Config.GetUserConfig().Git.Log.ShowGraph = value
697					gui.render()
698				}
699				return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
700					{
701						displayString: "always",
702						onPress: func() error {
703							onSelect("always")
704							return nil
705						},
706					},
707					{
708						displayString: "never",
709						onPress: func() error {
710							onSelect("never")
711							return nil
712						},
713					},
714					{
715						displayString: "when maximised",
716						onPress: func() error {
717							onSelect("when-maximised")
718							return nil
719						},
720					},
721				}, createMenuOptions{showCancel: true})
722			},
723		},
724		{
725			displayString: gui.Tr.SortCommits,
726			opensMenu:     true,
727			onPress: func() error {
728				onSelect := func(value string) error {
729					gui.Config.GetUserConfig().Git.Log.Order = value
730					return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
731						return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
732					})
733				}
734				return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
735					{
736						displayString: "topological (topo-order)",
737						onPress: func() error {
738							return onSelect("topo-order")
739						},
740					},
741					{
742						displayString: "date-order",
743						onPress: func() error {
744							return onSelect("date-order")
745						},
746					},
747					{
748						displayString: "author-date-order",
749						onPress: func() error {
750							return onSelect("author-date-order")
751						},
752					},
753				}, createMenuOptions{showCancel: true})
754			},
755		},
756	}, createMenuOptions{showCancel: true})
757}
758