github.com/andrewrech/lazygit@v0.8.1/pkg/gui/commits_panel.go (about) 1 package gui 2 3 import ( 4 "fmt" 5 "strconv" 6 7 "github.com/fatih/color" 8 "github.com/go-errors/errors" 9 10 "github.com/jesseduffield/gocui" 11 "github.com/jesseduffield/lazygit/pkg/commands" 12 "github.com/jesseduffield/lazygit/pkg/git" 13 "github.com/jesseduffield/lazygit/pkg/utils" 14 ) 15 16 // list panel functions 17 18 func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit { 19 selectedLine := gui.State.Panels.Commits.SelectedLine 20 if selectedLine == -1 { 21 return nil 22 } 23 24 return gui.State.Commits[selectedLine] 25 } 26 27 func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { 28 if gui.popupPanelFocused() { 29 return nil 30 } 31 32 if _, err := gui.g.SetCurrentView(v.Name()); err != nil { 33 return err 34 } 35 commit := gui.getSelectedCommit(g) 36 if commit == nil { 37 return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) 38 } 39 40 if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil { 41 return err 42 } 43 44 // if specific diff mode is on, don't show diff 45 if gui.State.Panels.Commits.SpecificDiffMode { 46 return nil 47 } 48 49 commitText, err := gui.GitCommand.Show(commit.Sha) 50 if err != nil { 51 return err 52 } 53 return gui.renderString(g, "main", commitText) 54 } 55 56 func (gui *Gui) refreshCommits(g *gocui.Gui) error { 57 g.Update(func(*gocui.Gui) error { 58 builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries) 59 if err != nil { 60 return err 61 } 62 commits, err := builder.GetCommits() 63 if err != nil { 64 return err 65 } 66 gui.State.Commits = commits 67 68 gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits)) 69 70 isFocused := gui.g.CurrentView().Name() == "commits" 71 list, err := utils.RenderList(gui.State.Commits, isFocused) 72 if err != nil { 73 return err 74 } 75 76 v := gui.getCommitsView() 77 v.Clear() 78 fmt.Fprint(v, list) 79 80 gui.refreshStatus(g) 81 if g.CurrentView() == v { 82 gui.handleCommitSelect(g, v) 83 } 84 if g.CurrentView() == gui.getCommitFilesView() { 85 return gui.refreshCommitFilesView() 86 } 87 return nil 88 }) 89 return nil 90 } 91 92 func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error { 93 if gui.popupPanelFocused() { 94 return nil 95 } 96 97 panelState := gui.State.Panels.Commits 98 gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false) 99 100 if err := gui.resetOrigin(gui.getMainView()); err != nil { 101 return err 102 } 103 return gui.handleCommitSelect(gui.g, v) 104 } 105 106 func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error { 107 if gui.popupPanelFocused() { 108 return nil 109 } 110 111 panelState := gui.State.Panels.Commits 112 gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true) 113 114 if err := gui.resetOrigin(gui.getMainView()); err != nil { 115 return err 116 } 117 return gui.handleCommitSelect(gui.g, v) 118 } 119 120 // specific functions 121 122 func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error { 123 return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error { 124 commit := gui.getSelectedCommit(g) 125 if commit == nil { 126 panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))) 127 } 128 129 if err := gui.GitCommand.ResetToCommit(commit.Sha, "mixed"); err != nil { 130 return gui.createErrorPanel(g, err.Error()) 131 } 132 if err := gui.refreshCommits(g); err != nil { 133 panic(err) 134 } 135 if err := gui.refreshFiles(); err != nil { 136 panic(err) 137 } 138 gui.resetOrigin(commitView) 139 gui.State.Panels.Commits.SelectedLine = 0 140 return gui.handleCommitSelect(g, commitView) 141 }, nil) 142 } 143 144 func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { 145 if len(gui.State.Commits) <= 1 { 146 return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) 147 } 148 149 applied, err := gui.handleMidRebaseCommand("squash") 150 if err != nil { 151 return err 152 } 153 if applied { 154 return nil 155 } 156 157 gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error { 158 return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error { 159 err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash") 160 return gui.handleGenericMergeCommandResult(err) 161 }) 162 }, nil) 163 return nil 164 } 165 166 // TODO: move to files panel 167 func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool { 168 for _, file := range files { 169 if file.Tracked && file.HasUnstagedChanges { 170 return true 171 } 172 } 173 return false 174 } 175 176 func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { 177 if len(gui.State.Commits) <= 1 { 178 return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) 179 } 180 181 applied, err := gui.handleMidRebaseCommand("fixup") 182 if err != nil { 183 return err 184 } 185 if applied { 186 return nil 187 } 188 189 gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error { 190 return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error { 191 err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup") 192 return gui.handleGenericMergeCommandResult(err) 193 }) 194 }, nil) 195 return nil 196 } 197 198 func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { 199 applied, err := gui.handleMidRebaseCommand("reword") 200 if err != nil { 201 return err 202 } 203 if applied { 204 return nil 205 } 206 207 if gui.State.Panels.Commits.SelectedLine != 0 { 208 return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) 209 } 210 return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error { 211 if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil { 212 return gui.createErrorPanel(g, err.Error()) 213 } 214 if err := gui.refreshCommits(g); err != nil { 215 panic(err) 216 } 217 return gui.handleCommitSelect(g, v) 218 }) 219 } 220 221 func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { 222 applied, err := gui.handleMidRebaseCommand("reword") 223 if err != nil { 224 return err 225 } 226 if applied { 227 return nil 228 } 229 230 subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine) 231 if err != nil { 232 return gui.createErrorPanel(gui.g, err.Error()) 233 } 234 if subProcess != nil { 235 gui.SubProcess = subProcess 236 return gui.Errors.ErrSubProcess 237 } 238 239 return nil 240 } 241 242 // handleMidRebaseCommand sees if the selected commit is in fact a rebasing 243 // commit meaning you are trying to edit the todo file rather than actually 244 // begin a rebase. It then updates the todo file with that action 245 func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) { 246 selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine] 247 if selectedCommit.Status != "rebasing" { 248 return false, nil 249 } 250 251 // for now we do not support setting 'reword' because it requires an editor 252 // and that means we either unconditionally wait around for the subprocess to ask for 253 // our input or we set a lazygit client as the EDITOR env variable and have it 254 // request us to edit the commit message when prompted. 255 if action == "reword" { 256 return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported")) 257 } 258 259 if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil { 260 return false, gui.createErrorPanel(gui.g, err.Error()) 261 } 262 return true, gui.refreshCommits(gui.g) 263 } 264 265 // handleMoveTodoDown like handleMidRebaseCommand but for moving an item up in the todo list 266 func (gui *Gui) handleMoveTodoDown(index int) (bool, error) { 267 selectedCommit := gui.State.Commits[index] 268 if selectedCommit.Status != "rebasing" { 269 return false, nil 270 } 271 if gui.State.Commits[index+1].Status != "rebasing" { 272 return true, nil 273 } 274 if err := gui.GitCommand.MoveTodoDown(index); err != nil { 275 return true, gui.createErrorPanel(gui.g, err.Error()) 276 } 277 return true, gui.refreshCommits(gui.g) 278 } 279 280 func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error { 281 applied, err := gui.handleMidRebaseCommand("drop") 282 if err != nil { 283 return err 284 } 285 if applied { 286 return nil 287 } 288 289 return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error { 290 return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error { 291 err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop") 292 return gui.handleGenericMergeCommandResult(err) 293 }) 294 }, nil) 295 } 296 297 func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error { 298 index := gui.State.Panels.Commits.SelectedLine 299 selectedCommit := gui.State.Commits[index] 300 if selectedCommit.Status == "rebasing" { 301 if gui.State.Commits[index+1].Status != "rebasing" { 302 return nil 303 } 304 if err := gui.GitCommand.MoveTodoDown(index); err != nil { 305 return gui.createErrorPanel(gui.g, err.Error()) 306 } 307 gui.State.Panels.Commits.SelectedLine++ 308 return gui.refreshCommits(gui.g) 309 } 310 311 return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error { 312 err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index) 313 if err == nil { 314 gui.State.Panels.Commits.SelectedLine++ 315 } 316 return gui.handleGenericMergeCommandResult(err) 317 }) 318 } 319 320 func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error { 321 index := gui.State.Panels.Commits.SelectedLine 322 if index == 0 { 323 return nil 324 } 325 selectedCommit := gui.State.Commits[index] 326 if selectedCommit.Status == "rebasing" { 327 if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil { 328 return gui.createErrorPanel(gui.g, err.Error()) 329 } 330 gui.State.Panels.Commits.SelectedLine-- 331 return gui.refreshCommits(gui.g) 332 } 333 334 return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error { 335 err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1) 336 if err == nil { 337 gui.State.Panels.Commits.SelectedLine-- 338 } 339 return gui.handleGenericMergeCommandResult(err) 340 }) 341 } 342 343 func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error { 344 applied, err := gui.handleMidRebaseCommand("edit") 345 if err != nil { 346 return err 347 } 348 if applied { 349 return nil 350 } 351 352 return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error { 353 err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit") 354 return gui.handleGenericMergeCommandResult(err) 355 }) 356 } 357 358 func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error { 359 return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error { 360 return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error { 361 err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha) 362 return gui.handleGenericMergeCommandResult(err) 363 }) 364 }, nil) 365 } 366 367 func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error { 368 applied, err := gui.handleMidRebaseCommand("pick") 369 if err != nil { 370 return err 371 } 372 if applied { 373 return nil 374 } 375 376 // at this point we aren't actually rebasing so we will interpret this as an 377 // attempt to pull. We might revoke this later after enabling configurable keybindings 378 return gui.pullFiles(g, v) 379 } 380 381 func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error { 382 if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil { 383 return gui.createErrorPanel(gui.g, err.Error()) 384 } 385 gui.State.Panels.Commits.SelectedLine++ 386 return gui.refreshCommits(gui.g) 387 } 388 389 func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error { 390 // get currently selected commit, add the sha to state. 391 commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine] 392 393 // we will un-copy it if it's already copied 394 for index, cherryPickedCommit := range gui.State.CherryPickedCommits { 395 if commit.Sha == cherryPickedCommit.Sha { 396 gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...) 397 return gui.refreshCommits(gui.g) 398 } 399 } 400 401 gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine) 402 return gui.refreshCommits(gui.g) 403 } 404 405 func (gui *Gui) addCommitToCherryPickedCommits(index int) { 406 // not super happy with modifying the state of the Commits array here 407 // but the alternative would be very tricky 408 gui.State.Commits[index].Copied = true 409 410 newCommits := []*commands.Commit{} 411 for _, commit := range gui.State.Commits { 412 if commit.Copied { 413 // duplicating just the things we need to put in the rebase TODO list 414 newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha}) 415 } 416 } 417 418 gui.State.CherryPickedCommits = newCommits 419 } 420 421 func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error { 422 // whenever I add a commit, I need to make sure I retain its order 423 424 // find the last commit that is copied that's above our position 425 // if there are none, startIndex = 0 426 startIndex := 0 427 for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] { 428 if commit.Copied { 429 startIndex = index 430 } 431 } 432 433 gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex)) 434 435 for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ { 436 gui.addCommitToCherryPickedCommits(index) 437 } 438 439 return gui.refreshCommits(gui.g) 440 } 441 442 // HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied 443 func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error { 444 return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error { 445 return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error { 446 err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits) 447 return gui.handleGenericMergeCommandResult(err) 448 }) 449 }, nil) 450 } 451 452 func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error { 453 if err := gui.refreshCommitFilesView(); err != nil { 454 return err 455 } 456 457 return gui.switchFocus(g, v, gui.getCommitFilesView()) 458 } 459 460 func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error { 461 selectLimit := 2 462 463 // get selected commit 464 commit := gui.getSelectedCommit(g) 465 if commit == nil { 466 return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) 467 } 468 469 // if already selected commit delete 470 if idx, has := gui.hasCommit(gui.State.DiffEntries, commit.Sha); has { 471 gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, idx) 472 } else { 473 if len(gui.State.DiffEntries) == selectLimit { 474 gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, 0) 475 } 476 gui.State.DiffEntries = append(gui.State.DiffEntries, commit) 477 } 478 479 gui.setDiffMode() 480 481 // if selected two commits, display diff between 482 if len(gui.State.DiffEntries) == selectLimit { 483 commitText, err := gui.GitCommand.DiffCommits(gui.State.DiffEntries[0].Sha, gui.State.DiffEntries[1].Sha) 484 485 if err != nil { 486 return gui.createErrorPanel(gui.g, err.Error()) 487 } 488 489 return gui.renderString(g, "main", commitText) 490 } 491 492 return nil 493 } 494 495 func (gui *Gui) setDiffMode() { 496 v := gui.getCommitsView() 497 if len(gui.State.DiffEntries) != 0 { 498 gui.State.Panels.Commits.SpecificDiffMode = true 499 v.Title = gui.Tr.SLocalize("CommitsDiffTitle") 500 } else { 501 gui.State.Panels.Commits.SpecificDiffMode = false 502 v.Title = gui.Tr.SLocalize("CommitsTitle") 503 } 504 505 gui.refreshCommits(gui.g) 506 } 507 508 func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) { 509 for idx, commit := range commits { 510 if commit.Sha == target { 511 return idx, true 512 } 513 } 514 return -1, false 515 } 516 517 func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Commit { 518 return append(commits[:i], commits[i+1:]...) 519 } 520 521 func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error { 522 commit := gui.getSelectedCommit(g) 523 if commit == nil { 524 return nil 525 } 526 527 return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CreateFixupCommit"), gui.Tr.TemplateLocalize( 528 "SureCreateFixupCommit", 529 Teml{ 530 "commit": commit.Sha, 531 }, 532 ), func(g *gocui.Gui, v *gocui.View) error { 533 if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil { 534 return gui.createErrorPanel(g, err.Error()) 535 } 536 537 return gui.refreshSidePanels(gui.g) 538 }, nil) 539 } 540 541 func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error { 542 commit := gui.getSelectedCommit(g) 543 if commit == nil { 544 return nil 545 } 546 547 return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize( 548 "SureSquashAboveCommits", 549 Teml{ 550 "commit": commit.Sha, 551 }, 552 ), func(g *gocui.Gui, v *gocui.View) error { 553 return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error { 554 err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha) 555 return gui.handleGenericMergeCommandResult(err) 556 }) 557 }, nil) 558 } 559 560 type resetOption struct { 561 description string 562 command string 563 } 564 565 // GetDisplayStrings is a function. 566 func (r *resetOption) GetDisplayStrings(isFocused bool) []string { 567 return []string{r.description, color.New(color.FgRed).Sprint(r.command)} 568 } 569 570 func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error { 571 commit := gui.getSelectedCommit(g) 572 if commit == nil { 573 return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch")) 574 } 575 576 strengths := []string{"soft", "mixed", "hard"} 577 options := make([]*resetOption, len(strengths)) 578 for i, strength := range strengths { 579 options[i] = &resetOption{ 580 description: fmt.Sprintf("%s reset", strength), 581 command: fmt.Sprintf("reset --%s %s", strength, commit.Sha), 582 } 583 } 584 585 handleMenuPress := func(index int) error { 586 if err := gui.GitCommand.ResetToCommit(commit.Sha, strengths[index]); err != nil { 587 return err 588 } 589 590 if err := gui.refreshCommits(g); err != nil { 591 return err 592 } 593 if err := gui.refreshFiles(); err != nil { 594 return err 595 } 596 if err := gui.resetOrigin(gui.getCommitsView()); err != nil { 597 return err 598 } 599 600 gui.State.Panels.Commits.SelectedLine = 0 601 return gui.handleCommitSelect(g, gui.getCommitsView()) 602 } 603 604 return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), commit.Sha), options, len(options), handleMenuPress) 605 }