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