github.com/andrewrech/lazygit@v0.8.1/pkg/gui/files_panel.go (about) 1 package gui 2 3 import ( 4 5 // "io" 6 // "io/ioutil" 7 8 // "strings" 9 10 "fmt" 11 "strings" 12 13 "github.com/fatih/color" 14 "github.com/jesseduffield/gocui" 15 "github.com/jesseduffield/lazygit/pkg/commands" 16 "github.com/jesseduffield/lazygit/pkg/utils" 17 ) 18 19 // list panel functions 20 21 func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) { 22 selectedLine := gui.State.Panels.Files.SelectedLine 23 if selectedLine == -1 { 24 return &commands.File{}, gui.Errors.ErrNoFiles 25 } 26 27 return gui.State.Files[selectedLine], nil 28 } 29 30 func (gui *Gui) handleFilesFocus(g *gocui.Gui, v *gocui.View) error { 31 if gui.popupPanelFocused() { 32 return nil 33 } 34 35 cx, cy := v.Cursor() 36 _, oy := v.Origin() 37 38 prevSelectedLine := gui.State.Panels.Files.SelectedLine 39 newSelectedLine := cy - oy 40 41 if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx { 42 return gui.handleFileSelect(gui.g, v, false) 43 } 44 45 gui.State.Panels.Files.SelectedLine = newSelectedLine 46 47 if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() { 48 return gui.handleFilePress(gui.g, v) 49 } else { 50 return gui.handleFileSelect(gui.g, v, true) 51 } 52 } 53 54 func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error { 55 if _, err := gui.g.SetCurrentView(v.Name()); err != nil { 56 return err 57 } 58 59 file, err := gui.getSelectedFile(g) 60 if err != nil { 61 if err != gui.Errors.ErrNoFiles { 62 return err 63 } 64 return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles")) 65 } 66 67 if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), v); err != nil { 68 return err 69 } 70 71 if file.HasInlineMergeConflicts { 72 return gui.refreshMergePanel() 73 } 74 75 content := gui.GitCommand.Diff(file, false) 76 if alreadySelected { 77 g.Update(func(*gocui.Gui) error { 78 return gui.setViewContent(gui.g, gui.getMainView(), content) 79 }) 80 return nil 81 } 82 return gui.renderString(g, "main", content) 83 } 84 85 func (gui *Gui) refreshFiles() error { 86 selectedFile, _ := gui.getSelectedFile(gui.g) 87 88 filesView := gui.getFilesView() 89 if filesView == nil { 90 // if the filesView hasn't been instantiated yet we just return 91 return nil 92 } 93 if err := gui.refreshStateFiles(); err != nil { 94 return err 95 } 96 97 gui.g.Update(func(g *gocui.Gui) error { 98 99 filesView.Clear() 100 isFocused := gui.g.CurrentView().Name() == "files" 101 list, err := utils.RenderList(gui.State.Files, isFocused) 102 if err != nil { 103 return err 104 } 105 fmt.Fprint(filesView, list) 106 107 if filesView == g.CurrentView() { 108 newSelectedFile, _ := gui.getSelectedFile(gui.g) 109 alreadySelected := newSelectedFile.Name == selectedFile.Name 110 return gui.handleFileSelect(g, filesView, alreadySelected) 111 } 112 return nil 113 }) 114 115 return nil 116 } 117 118 func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error { 119 if gui.popupPanelFocused() { 120 return nil 121 } 122 123 panelState := gui.State.Panels.Files 124 gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false) 125 126 return gui.handleFileSelect(gui.g, v, false) 127 } 128 129 func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error { 130 if gui.popupPanelFocused() { 131 return nil 132 } 133 134 panelState := gui.State.Panels.Files 135 gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true) 136 137 return gui.handleFileSelect(gui.g, v, false) 138 } 139 140 // specific functions 141 142 func (gui *Gui) stagedFiles() []*commands.File { 143 files := gui.State.Files 144 result := make([]*commands.File, 0) 145 for _, file := range files { 146 if file.HasStagedChanges { 147 result = append(result, file) 148 } 149 } 150 return result 151 } 152 153 func (gui *Gui) trackedFiles() []*commands.File { 154 files := gui.State.Files 155 result := make([]*commands.File, 0) 156 for _, file := range files { 157 if file.Tracked { 158 result = append(result, file) 159 } 160 } 161 return result 162 } 163 164 func (gui *Gui) stageSelectedFile(g *gocui.Gui) error { 165 file, err := gui.getSelectedFile(g) 166 if err != nil { 167 return err 168 } 169 return gui.GitCommand.StageFile(file.Name) 170 } 171 172 func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error { 173 file, err := gui.getSelectedFile(g) 174 if err != nil { 175 if err != gui.Errors.ErrNoFiles { 176 return err 177 } 178 return nil 179 } 180 if file.HasInlineMergeConflicts { 181 return gui.handleSwitchToMerge(g, v) 182 } 183 if !file.HasUnstagedChanges || file.HasMergeConflicts { 184 return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements")) 185 } 186 if err := gui.changeContext("main", "staging"); err != nil { 187 return err 188 } 189 if err := gui.switchFocus(g, v, gui.getMainView()); err != nil { 190 return err 191 } 192 return gui.refreshStagingPanel() 193 } 194 195 func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error { 196 file, err := gui.getSelectedFile(g) 197 if err != nil { 198 if err == gui.Errors.ErrNoFiles { 199 return nil 200 } 201 return err 202 } 203 204 if file.HasInlineMergeConflicts { 205 return gui.handleSwitchToMerge(g, v) 206 } 207 208 if file.HasUnstagedChanges { 209 gui.GitCommand.StageFile(file.Name) 210 } else { 211 gui.GitCommand.UnStageFile(file.Name, file.Tracked) 212 } 213 214 if err := gui.refreshFiles(); err != nil { 215 return err 216 } 217 218 return gui.handleFileSelect(g, v, true) 219 } 220 221 func (gui *Gui) allFilesStaged() bool { 222 for _, file := range gui.State.Files { 223 if file.HasUnstagedChanges { 224 return false 225 } 226 } 227 return true 228 } 229 230 func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error { 231 var err error 232 if gui.allFilesStaged() { 233 err = gui.GitCommand.UnstageAll() 234 } else { 235 err = gui.GitCommand.StageAll() 236 } 237 if err != nil { 238 _ = gui.createErrorPanel(g, err.Error()) 239 } 240 241 if err := gui.refreshFiles(); err != nil { 242 return err 243 } 244 245 return gui.handleFileSelect(g, v, false) 246 } 247 248 func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error { 249 file, err := gui.getSelectedFile(g) 250 if err != nil { 251 if err == gui.Errors.ErrNoFiles { 252 return nil 253 } 254 return err 255 } 256 if !file.HasUnstagedChanges { 257 return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges")) 258 } 259 if !file.Tracked { 260 return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd")) 261 } 262 263 gui.SubProcess = gui.GitCommand.AddPatch(file.Name) 264 return gui.Errors.ErrSubProcess 265 } 266 267 func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { 268 file, err := gui.getSelectedFile(g) 269 if err != nil { 270 return gui.createErrorPanel(g, err.Error()) 271 } 272 if file.Tracked { 273 return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles")) 274 } 275 if err := gui.GitCommand.Ignore(file.Name); err != nil { 276 return gui.createErrorPanel(g, err.Error()) 277 } 278 return gui.refreshFiles() 279 } 280 281 func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error { 282 skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix") 283 if skipHookPreifx == "" { 284 return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured")) 285 } 286 287 if err := gui.renderString(g, "commitMessage", skipHookPreifx); err != nil { 288 return err 289 } 290 if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil { 291 return err 292 } 293 294 return gui.handleCommitPress(g, filesView) 295 } 296 297 func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { 298 if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" { 299 return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) 300 } 301 commitMessageView := gui.getCommitMessageView() 302 g.Update(func(g *gocui.Gui) error { 303 g.SetViewOnTop("commitMessage") 304 gui.switchFocus(g, filesView, commitMessageView) 305 gui.RenderCommitLength() 306 return nil 307 }) 308 return nil 309 } 310 311 func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error { 312 if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" { 313 return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) 314 } 315 if len(gui.State.Commits) == 0 { 316 return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend")) 317 } 318 319 title := strings.Title(gui.Tr.SLocalize("AmendLastCommit")) 320 question := gui.Tr.SLocalize("SureToAmend") 321 322 return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error { 323 ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead()) 324 if err != nil { 325 return err 326 } 327 if !ok { 328 return nil 329 } 330 331 return gui.refreshSidePanels(g) 332 }, nil) 333 } 334 335 // handleCommitEditorPress - handle when the user wants to commit changes via 336 // their editor rather than via the popup panel 337 func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error { 338 if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" { 339 return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit")) 340 } 341 gui.PrepareSubProcess(g, "git", "commit") 342 return nil 343 } 344 345 // PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it 346 func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) { 347 gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess() 348 g.Update(func(g *gocui.Gui) error { 349 return gui.Errors.ErrSubProcess 350 }) 351 } 352 353 func (gui *Gui) editFile(filename string) error { 354 _, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename)) 355 return err 356 } 357 358 func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error { 359 file, err := gui.getSelectedFile(g) 360 if err != nil { 361 return gui.createErrorPanel(gui.g, err.Error()) 362 } 363 364 return gui.editFile(file.Name) 365 } 366 367 func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error { 368 file, err := gui.getSelectedFile(g) 369 if err != nil { 370 return gui.createErrorPanel(gui.g, err.Error()) 371 } 372 return gui.openFile(file.Name) 373 } 374 375 func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { 376 return gui.refreshFiles() 377 } 378 379 func (gui *Gui) refreshStateFiles() error { 380 // get files to stage 381 files := gui.GitCommand.GetStatusFiles() 382 gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files) 383 gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files)) 384 return gui.updateWorkTreeState() 385 } 386 387 func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { 388 item, err := gui.getSelectedFile(g) 389 if err != nil { 390 if err != gui.Errors.ErrNoFiles { 391 return "", err 392 } 393 return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay")) 394 } 395 if item.Type != "file" { 396 return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile")) 397 } 398 cat, err := gui.GitCommand.CatFile(item.Name) 399 if err != nil { 400 gui.Log.Error(err) 401 return "", gui.renderString(g, "main", err.Error()) 402 } 403 return cat, nil 404 } 405 406 func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { 407 if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil { 408 return err 409 } 410 411 go func() { 412 unamePassOpend := false 413 err := gui.GitCommand.Pull(func(passOrUname string) string { 414 unamePassOpend = true 415 return gui.waitForPassUname(g, v, passOrUname) 416 }) 417 gui.HandleCredentialsPopup(g, unamePassOpend, err) 418 }() 419 return nil 420 } 421 422 func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error { 423 if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil { 424 return err 425 } 426 go func() { 427 unamePassOpend := false 428 branchName := gui.State.Branches[0].Name 429 err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string { 430 unamePassOpend = true 431 return gui.waitForPassUname(g, v, passOrUname) 432 }) 433 gui.HandleCredentialsPopup(g, unamePassOpend, err) 434 }() 435 return nil 436 } 437 438 func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error { 439 // if we have pullables we'll ask if the user wants to force push 440 _, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() 441 if pullables == "?" || pullables == "0" { 442 return gui.pushWithForceFlag(g, v, false) 443 } 444 err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error { 445 return gui.pushWithForceFlag(g, v, true) 446 }, nil) 447 return err 448 } 449 450 func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { 451 file, err := gui.getSelectedFile(g) 452 if err != nil { 453 if err != gui.Errors.ErrNoFiles { 454 return gui.createErrorPanel(gui.g, err.Error()) 455 } 456 return nil 457 } 458 if !file.HasInlineMergeConflicts { 459 return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons")) 460 } 461 if err := gui.changeContext("main", "merging"); err != nil { 462 return err 463 } 464 if err := gui.switchFocus(g, v, gui.getMainView()); err != nil { 465 return err 466 } 467 return gui.refreshMergePanel() 468 } 469 470 func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error { 471 if err := gui.GitCommand.AbortMerge(); err != nil { 472 return gui.createErrorPanel(g, err.Error()) 473 } 474 gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted")) 475 gui.refreshStatus(g) 476 return gui.refreshFiles() 477 } 478 479 func (gui *Gui) openFile(filename string) error { 480 if err := gui.OSCommand.OpenFile(filename); err != nil { 481 return gui.createErrorPanel(gui.g, err.Error()) 482 } 483 return nil 484 } 485 486 func (gui *Gui) anyFilesWithMergeConflicts() bool { 487 for _, file := range gui.State.Files { 488 if file.HasMergeConflicts { 489 return true 490 } 491 } 492 return false 493 } 494 495 type discardOption struct { 496 handler func(fileName *commands.File) error 497 description string 498 } 499 500 type discardAllOption struct { 501 handler func() error 502 description string 503 command string 504 } 505 506 // GetDisplayStrings is a function. 507 func (r *discardOption) GetDisplayStrings(isFocused bool) []string { 508 return []string{r.description} 509 } 510 511 // GetDisplayStrings is a function. 512 func (r *discardAllOption) GetDisplayStrings(isFocused bool) []string { 513 return []string{r.description, color.New(color.FgRed).Sprint(r.command)} 514 } 515 516 func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error { 517 file, err := gui.getSelectedFile(g) 518 if err != nil { 519 if err != gui.Errors.ErrNoFiles { 520 return err 521 } 522 return nil 523 } 524 525 options := []*discardOption{ 526 { 527 description: gui.Tr.SLocalize("discardAllChanges"), 528 handler: func(file *commands.File) error { 529 return gui.GitCommand.DiscardAllFileChanges(file) 530 }, 531 }, 532 { 533 description: gui.Tr.SLocalize("cancel"), 534 handler: func(file *commands.File) error { 535 return nil 536 }, 537 }, 538 } 539 540 if file.HasStagedChanges && file.HasUnstagedChanges { 541 discardUnstagedChanges := &discardOption{ 542 description: gui.Tr.SLocalize("discardUnstagedChanges"), 543 handler: func(file *commands.File) error { 544 return gui.GitCommand.DiscardUnstagedFileChanges(file) 545 }, 546 } 547 548 options = append(options[:1], append([]*discardOption{discardUnstagedChanges}, options[1:]...)...) 549 } 550 551 handleMenuPress := func(index int) error { 552 file, err := gui.getSelectedFile(g) 553 if err != nil { 554 return err 555 } 556 557 if err := options[index].handler(file); err != nil { 558 return err 559 } 560 561 return gui.refreshFiles() 562 } 563 564 return gui.createMenu(file.Name, options, len(options), handleMenuPress) 565 } 566 567 func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error { 568 options := []*discardAllOption{ 569 { 570 description: gui.Tr.SLocalize("discardAllChangesToAllFiles"), 571 command: "reset --hard HEAD && git clean -fd", 572 handler: func() error { 573 return gui.GitCommand.ResetAndClean() 574 }, 575 }, 576 { 577 description: gui.Tr.SLocalize("discardAnyUnstagedChanges"), 578 command: "git checkout -- .", 579 handler: func() error { 580 return gui.GitCommand.DiscardAnyUnstagedFileChanges() 581 }, 582 }, 583 { 584 description: gui.Tr.SLocalize("discardUntrackedFiles"), 585 command: "git clean -fd", 586 handler: func() error { 587 return gui.GitCommand.RemoveUntrackedFiles() 588 }, 589 }, 590 { 591 description: gui.Tr.SLocalize("softReset"), 592 command: "git reset --soft HEAD", 593 handler: func() error { 594 return gui.GitCommand.ResetSoftHead() 595 }, 596 }, 597 { 598 description: gui.Tr.SLocalize("hardReset"), 599 command: "git reset --hard HEAD", 600 handler: func() error { 601 return gui.GitCommand.ResetHardHead() 602 }, 603 }, 604 { 605 description: gui.Tr.SLocalize("cancel"), 606 handler: func() error { 607 return nil 608 }, 609 }, 610 } 611 612 handleMenuPress := func(index int) error { 613 if err := options[index].handler(); err != nil { 614 return err 615 } 616 617 return gui.refreshFiles() 618 } 619 620 return gui.createMenu("", options, len(options), handleMenuPress) 621 } 622 623 func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { 624 return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), func(g *gocui.Gui, v *gocui.View) error { 625 command := gui.trimmedContent(v) 626 gui.SubProcess = gui.OSCommand.RunCustomCommand(command) 627 return gui.Errors.ErrSubProcess 628 }) 629 } 630 631 type stashOption struct { 632 description string 633 handler func() error 634 } 635 636 // GetDisplayStrings is a function. 637 func (o *stashOption) GetDisplayStrings(isFocused bool) []string { 638 return []string{o.description} 639 } 640 641 func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error { 642 options := []*stashOption{ 643 { 644 description: gui.Tr.SLocalize("stashAllChanges"), 645 handler: func() error { 646 return gui.handleStashSave(gui.GitCommand.StashSave) 647 }, 648 }, 649 { 650 description: gui.Tr.SLocalize("stashStagedChanges"), 651 handler: func() error { 652 return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges) 653 }, 654 }, 655 { 656 description: gui.Tr.SLocalize("cancel"), 657 handler: func() error { 658 return nil 659 }, 660 }, 661 } 662 663 handleMenuPress := func(index int) error { 664 return options[index].handler() 665 } 666 667 return gui.createMenu(gui.Tr.SLocalize("stashOptions"), options, len(options), handleMenuPress) 668 } 669 670 func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error { 671 return gui.handleStashSave(gui.GitCommand.StashSave) 672 }