github.com/Ryooooooga/lazygit@v0.8.1/pkg/commands/git.go (about) 1 package commands 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "os/exec" 8 "strings" 9 10 "github.com/mgutz/str" 11 12 "github.com/go-errors/errors" 13 14 "github.com/jesseduffield/lazygit/pkg/config" 15 "github.com/jesseduffield/lazygit/pkg/i18n" 16 "github.com/jesseduffield/lazygit/pkg/utils" 17 "github.com/sirupsen/logrus" 18 gitconfig "github.com/tcnksm/go-gitconfig" 19 gogit "gopkg.in/src-d/go-git.v4" 20 ) 21 22 func verifyInGitRepo(runCmd func(string) error) error { 23 return runCmd("git status") 24 } 25 26 func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error { 27 for { 28 _, err := stat(".git") 29 30 if err == nil { 31 return nil 32 } 33 34 if !os.IsNotExist(err) { 35 return WrapError(err) 36 } 37 38 if err = chdir(".."); err != nil { 39 return WrapError(err) 40 } 41 } 42 } 43 44 func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) { 45 repository, err = openGitRepository(".") 46 47 if err != nil { 48 if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) { 49 return nil, nil, errors.New(sLocalize("GitconfigParseErr")) 50 } 51 52 return 53 } 54 55 worktree, err = repository.Worktree() 56 57 if err != nil { 58 return 59 } 60 61 return 62 } 63 64 // GitCommand is our main git interface 65 type GitCommand struct { 66 Log *logrus.Entry 67 OSCommand *OSCommand 68 Worktree *gogit.Worktree 69 Repo *gogit.Repository 70 Tr *i18n.Localizer 71 Config config.AppConfigurer 72 getGlobalGitConfig func(string) (string, error) 73 getLocalGitConfig func(string) (string, error) 74 removeFile func(string) error 75 DotGitDir string 76 } 77 78 // NewGitCommand it runs git commands 79 func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*GitCommand, error) { 80 var worktree *gogit.Worktree 81 var repo *gogit.Repository 82 83 fs := []func() error{ 84 func() error { 85 return verifyInGitRepo(osCommand.RunCommand) 86 }, 87 func() error { 88 return navigateToRepoRootDirectory(os.Stat, os.Chdir) 89 }, 90 func() error { 91 var err error 92 repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize) 93 return err 94 }, 95 } 96 97 for _, f := range fs { 98 if err := f(); err != nil { 99 return nil, err 100 } 101 } 102 103 dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile) 104 if err != nil { 105 return nil, err 106 } 107 108 return &GitCommand{ 109 Log: log, 110 OSCommand: osCommand, 111 Tr: tr, 112 Worktree: worktree, 113 Repo: repo, 114 Config: config, 115 getGlobalGitConfig: gitconfig.Global, 116 getLocalGitConfig: gitconfig.Local, 117 removeFile: os.RemoveAll, 118 DotGitDir: dotGitDir, 119 }, nil 120 } 121 122 func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) { 123 f, err := stat(".git") 124 if err != nil { 125 return "", err 126 } 127 128 if f.IsDir() { 129 return ".git", nil 130 } 131 132 fileBytes, err := readFile(".git") 133 if err != nil { 134 return "", err 135 } 136 fileContent := string(fileBytes) 137 if !strings.HasPrefix(fileContent, "gitdir: ") { 138 return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory") 139 } 140 return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil 141 } 142 143 // GetStashEntries stash entryies 144 func (c *GitCommand) GetStashEntries() []*StashEntry { 145 rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'") 146 stashEntries := []*StashEntry{} 147 for i, line := range utils.SplitLines(rawString) { 148 stashEntries = append(stashEntries, stashEntryFromLine(line, i)) 149 } 150 return stashEntries 151 } 152 153 func stashEntryFromLine(line string, index int) *StashEntry { 154 return &StashEntry{ 155 Name: line, 156 Index: index, 157 DisplayString: line, 158 } 159 } 160 161 // GetStashEntryDiff stash diff 162 func (c *GitCommand) GetStashEntryDiff(index int) (string, error) { 163 return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") 164 } 165 166 // GetStatusFiles git status files 167 func (c *GitCommand) GetStatusFiles() []*File { 168 statusOutput, _ := c.GitStatus() 169 statusStrings := utils.SplitLines(statusOutput) 170 files := []*File{} 171 172 for _, statusString := range statusStrings { 173 change := statusString[0:2] 174 stagedChange := change[0:1] 175 unstagedChange := statusString[1:2] 176 filename := c.OSCommand.Unquote(statusString[3:]) 177 _, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change] 178 _, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange] 179 180 file := &File{ 181 Name: filename, 182 DisplayString: statusString, 183 HasStagedChanges: !hasNoStagedChanges, 184 HasUnstagedChanges: unstagedChange != " ", 185 Tracked: !untracked, 186 Deleted: unstagedChange == "D" || stagedChange == "D", 187 HasMergeConflicts: change == "UU" || change == "AA" || change == "DU", 188 HasInlineMergeConflicts: change == "UU" || change == "AA", 189 Type: c.OSCommand.FileType(filename), 190 ShortStatus: change, 191 } 192 files = append(files, file) 193 } 194 return files 195 } 196 197 // StashDo modify stash 198 func (c *GitCommand) StashDo(index int, method string) error { 199 return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index)) 200 } 201 202 // StashSave save stash 203 // TODO: before calling this, check if there is anything to save 204 func (c *GitCommand) StashSave(message string) error { 205 return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message))) 206 } 207 208 // MergeStatusFiles merge status files 209 func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File { 210 if len(oldFiles) == 0 { 211 return newFiles 212 } 213 214 appendedIndexes := []int{} 215 216 // retain position of files we already could see 217 result := []*File{} 218 for _, oldFile := range oldFiles { 219 for newIndex, newFile := range newFiles { 220 if oldFile.Name == newFile.Name { 221 result = append(result, newFile) 222 appendedIndexes = append(appendedIndexes, newIndex) 223 break 224 } 225 } 226 } 227 228 // append any new files to the end 229 for index, newFile := range newFiles { 230 if !includesInt(appendedIndexes, index) { 231 result = append(result, newFile) 232 } 233 } 234 235 return result 236 } 237 238 func includesInt(list []int, a int) bool { 239 for _, b := range list { 240 if b == a { 241 return true 242 } 243 } 244 return false 245 } 246 247 // ResetAndClean removes all unstaged changes and removes all untracked files 248 func (c *GitCommand) ResetAndClean() error { 249 if err := c.ResetHardHead(); err != nil { 250 return err 251 } 252 253 return c.RemoveUntrackedFiles() 254 } 255 256 func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) { 257 return c.GetCommitDifferences("HEAD", "@{u}") 258 } 259 260 func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) { 261 upstream := "origin" // hardcoded for now 262 return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName)) 263 } 264 265 // GetCommitDifferences checks how many pushables/pullables there are for the 266 // current branch 267 func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { 268 command := "git rev-list %s..%s --count" 269 pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from)) 270 if err != nil { 271 return "?", "?" 272 } 273 pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to)) 274 if err != nil { 275 return "?", "?" 276 } 277 return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) 278 } 279 280 // RenameCommit renames the topmost commit with the given name 281 func (c *GitCommand) RenameCommit(name string) error { 282 return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))) 283 } 284 285 // RebaseBranch interactive rebases onto a branch 286 func (c *GitCommand) RebaseBranch(branchName string) error { 287 cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) 288 if err != nil { 289 return err 290 } 291 292 return c.OSCommand.RunPreparedCommand(cmd) 293 } 294 295 // Fetch fetch git repo 296 func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error { 297 return c.OSCommand.DetectUnamePass("git fetch", func(question string) string { 298 if canAskForCredentials { 299 return unamePassQuestion(question) 300 } 301 return "\n" 302 }) 303 } 304 305 // ResetToCommit reset to commit 306 func (c *GitCommand) ResetToCommit(sha string, strength string) error { 307 return c.OSCommand.RunCommand(fmt.Sprintf("git reset --%s %s", strength, sha)) 308 } 309 310 // NewBranch create new branch 311 func (c *GitCommand) NewBranch(name string) error { 312 return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name)) 313 } 314 315 // CurrentBranchName is a function. 316 func (c *GitCommand) CurrentBranchName() (string, error) { 317 branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD") 318 if err != nil { 319 branchName, err = c.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD") 320 if err != nil { 321 return "", err 322 } 323 } 324 return utils.TrimTrailingNewline(branchName), nil 325 } 326 327 // DeleteBranch delete branch 328 func (c *GitCommand) DeleteBranch(branch string, force bool) error { 329 command := "git branch -d" 330 331 if force { 332 command = "git branch -D" 333 } 334 335 return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch)) 336 } 337 338 // ListStash list stash 339 func (c *GitCommand) ListStash() (string, error) { 340 return c.OSCommand.RunCommandWithOutput("git stash list") 341 } 342 343 // Merge merge 344 func (c *GitCommand) Merge(branchName string) error { 345 return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName)) 346 } 347 348 // AbortMerge abort merge 349 func (c *GitCommand) AbortMerge() error { 350 return c.OSCommand.RunCommand("git merge --abort") 351 } 352 353 // usingGpg tells us whether the user has gpg enabled so that we can know 354 // whether we need to run a subprocess to allow them to enter their password 355 func (c *GitCommand) usingGpg() bool { 356 gpgsign, _ := c.getLocalGitConfig("commit.gpgsign") 357 if gpgsign == "" { 358 gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign") 359 } 360 value := strings.ToLower(gpgsign) 361 362 return value == "true" || value == "1" || value == "yes" || value == "on" 363 } 364 365 // Commit commits to git 366 func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) { 367 command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message)) 368 if c.usingGpg() { 369 return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil 370 } 371 372 return nil, c.OSCommand.RunCommand(command) 373 } 374 375 // AmendHead amends HEAD with whatever is staged in your working tree 376 func (c *GitCommand) AmendHead() (*exec.Cmd, error) { 377 command := "git commit --amend --no-edit" 378 if c.usingGpg() { 379 return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil 380 } 381 382 return nil, c.OSCommand.RunCommand(command) 383 } 384 385 // Pull pulls from repo 386 func (c *GitCommand) Pull(ask func(string) string) error { 387 return c.OSCommand.DetectUnamePass("git pull --no-edit", ask) 388 } 389 390 // Push pushes to a branch 391 func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error { 392 forceFlag := "" 393 if force { 394 forceFlag = "--force-with-lease " 395 } 396 397 cmd := fmt.Sprintf("git push %s-u origin %s", forceFlag, branchName) 398 return c.OSCommand.DetectUnamePass(cmd, ask) 399 } 400 401 // CatFile obtains the content of a file 402 func (c *GitCommand) CatFile(fileName string) (string, error) { 403 return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName))) 404 } 405 406 // StageFile stages a file 407 func (c *GitCommand) StageFile(fileName string) error { 408 return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName))) 409 } 410 411 // StageAll stages all files 412 func (c *GitCommand) StageAll() error { 413 return c.OSCommand.RunCommand("git add -A") 414 } 415 416 // UnstageAll stages all files 417 func (c *GitCommand) UnstageAll() error { 418 return c.OSCommand.RunCommand("git reset") 419 } 420 421 // UnStageFile unstages a file 422 func (c *GitCommand) UnStageFile(fileName string, tracked bool) error { 423 command := "git rm --cached %s" 424 if tracked { 425 command = "git reset HEAD %s" 426 } 427 428 // renamed files look like "file1 -> file2" 429 fileNames := strings.Split(fileName, " -> ") 430 for _, name := range fileNames { 431 if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil { 432 return err 433 } 434 } 435 return nil 436 } 437 438 // GitStatus returns the plaintext short status of the repo 439 func (c *GitCommand) GitStatus() (string, error) { 440 return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain") 441 } 442 443 // IsInMergeState states whether we are still mid-merge 444 func (c *GitCommand) IsInMergeState() (bool, error) { 445 output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all") 446 if err != nil { 447 return false, err 448 } 449 return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil 450 } 451 452 // RebaseMode returns "" for non-rebase mode, "normal" for normal rebase 453 // and "interactive" for interactive rebase 454 func (c *GitCommand) RebaseMode() (string, error) { 455 exists, err := c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-apply", c.DotGitDir)) 456 if err != nil { 457 return "", err 458 } 459 if exists { 460 return "normal", nil 461 } 462 exists, err = c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-merge", c.DotGitDir)) 463 if exists { 464 return "interactive", err 465 } else { 466 return "", err 467 } 468 } 469 470 // DiscardAllFileChanges directly 471 func (c *GitCommand) DiscardAllFileChanges(file *File) error { 472 // if the file isn't tracked, we assume you want to delete it 473 quotedFileName := c.OSCommand.Quote(file.Name) 474 if file.HasStagedChanges { 475 if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil { 476 return err 477 } 478 } 479 if !file.Tracked { 480 return c.removeFile(file.Name) 481 } 482 return c.DiscardUnstagedFileChanges(file) 483 } 484 485 // DiscardUnstagedFileChanges directly 486 func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error { 487 quotedFileName := c.OSCommand.Quote(file.Name) 488 return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName)) 489 } 490 491 // Checkout checks out a branch, with --force if you set the force arg to true 492 func (c *GitCommand) Checkout(branch string, force bool) error { 493 forceArg := "" 494 if force { 495 forceArg = "--force " 496 } 497 return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", forceArg, branch)) 498 } 499 500 // AddPatch prepares a subprocess for adding a patch by patch 501 // this will eventually be swapped out for a better solution inside the Gui 502 func (c *GitCommand) AddPatch(filename string) *exec.Cmd { 503 return c.OSCommand.PrepareSubProcess("git", "add", "--patch", c.OSCommand.Quote(filename)) 504 } 505 506 // PrepareCommitSubProcess prepares a subprocess for `git commit` 507 func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd { 508 return c.OSCommand.PrepareSubProcess("git", "commit") 509 } 510 511 // PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty` 512 func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd { 513 return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty") 514 } 515 516 // GetBranchGraph gets the color-formatted graph of the log for the given branch 517 // Currently it limits the result to 100 commits, but when we get async stuff 518 // working we can do lazy loading 519 func (c *GitCommand) GetBranchGraph(branchName string) (string, error) { 520 return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName)) 521 } 522 523 // Ignore adds a file to the gitignore for the repo 524 func (c *GitCommand) Ignore(filename string) error { 525 return c.OSCommand.AppendLineToFile(".gitignore", filename) 526 } 527 528 // Show shows the diff of a commit 529 func (c *GitCommand) Show(sha string) (string, error) { 530 show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha)) 531 if err != nil { 532 return "", err 533 } 534 535 // if this is a merge commit, we need to go a step further and get the diff between the two branches we merged 536 revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha)) 537 if err != nil { 538 // turns out we get an error here when it's the first commit. We'll just return the original show 539 return show, nil 540 } 541 if len(revList) == 0 { 542 return show, nil 543 } 544 545 // we want to pull out 1a6a69a and 3b51d7c from this: 546 // commit ccc771d8b13d5b0d4635db4463556366470fd4f6 547 // Merge: 1a6a69a 3b51d7c 548 lines := utils.SplitLines(show) 549 if len(lines) < 2 { 550 return show, nil 551 } 552 553 secondLineWords := strings.Split(lines[1], " ") 554 if len(secondLineWords) < 3 { 555 return show, nil 556 } 557 558 mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2])) 559 if err != nil { 560 return "", err 561 } 562 return show + mergeDiff, nil 563 } 564 565 // GetRemoteURL returns current repo remote url 566 func (c *GitCommand) GetRemoteURL() string { 567 url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url") 568 return utils.TrimTrailingNewline(url) 569 } 570 571 // CheckRemoteBranchExists Returns remote branch 572 func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool { 573 _, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf( 574 "git show-ref --verify -- refs/remotes/origin/%s", 575 branch.Name, 576 )) 577 578 return err == nil 579 } 580 581 // Diff returns the diff of a file 582 func (c *GitCommand) Diff(file *File, plain bool) string { 583 cachedArg := "" 584 trackedArg := "--" 585 colorArg := "--color" 586 split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename 587 fileName := c.OSCommand.Quote(split[len(split)-1]) 588 if file.HasStagedChanges && !file.HasUnstagedChanges { 589 cachedArg = "--cached" 590 } 591 if !file.Tracked && !file.HasStagedChanges { 592 trackedArg = "--no-index /dev/null" 593 } 594 if plain { 595 colorArg = "" 596 } 597 598 command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName) 599 600 // for now we assume an error means the file was deleted 601 s, _ := c.OSCommand.RunCommandWithOutput(command) 602 return s 603 } 604 605 func (c *GitCommand) ApplyPatch(patch string) (string, error) { 606 filename, err := c.OSCommand.CreateTempFile("patch", patch) 607 if err != nil { 608 c.Log.Error(err) 609 return "", err 610 } 611 612 defer func() { _ = c.OSCommand.Remove(filename) }() 613 614 return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", c.OSCommand.Quote(filename))) 615 } 616 617 func (c *GitCommand) FastForward(branchName string) error { 618 upstream := "origin" // hardcoding for now 619 return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName)) 620 } 621 622 func (c *GitCommand) RunSkipEditorCommand(command string) error { 623 cmd := c.OSCommand.ExecutableFromString(command) 624 cmd.Env = append( 625 cmd.Env, 626 "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY", 627 "EDITOR="+c.OSCommand.GetLazygitPath(), 628 ) 629 return c.OSCommand.RunExecutable(cmd) 630 } 631 632 // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" 633 // By default we skip the editor in the case where a commit will be made 634 func (c *GitCommand) GenericMerge(commandType string, command string) error { 635 return c.RunSkipEditorCommand( 636 fmt.Sprintf( 637 "git %s --%s", 638 commandType, 639 command, 640 ), 641 ) 642 } 643 644 func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) { 645 todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword") 646 if err != nil { 647 return nil, err 648 } 649 650 return c.PrepareInteractiveRebaseCommand(sha, todo, false) 651 } 652 653 func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error { 654 // we must ensure that we have at least two commits after the selected one 655 if len(commits) <= index+2 { 656 // assuming they aren't picking the bottom commit 657 return errors.New(c.Tr.SLocalize("NoRoom")) 658 } 659 660 todo := "" 661 orderedCommits := append(commits[0:index], commits[index+1], commits[index]) 662 for _, commit := range orderedCommits { 663 todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo 664 } 665 666 cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) 667 if err != nil { 668 return err 669 } 670 671 return c.OSCommand.RunPreparedCommand(cmd) 672 } 673 674 func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error { 675 todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action) 676 if err != nil { 677 return err 678 } 679 680 cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) 681 if err != nil { 682 return err 683 } 684 685 return c.OSCommand.RunPreparedCommand(cmd) 686 } 687 688 // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase 689 // we tell git to run lazygit to edit the todo list, and we pass the client 690 // lazygit a todo string to write to the todo file 691 func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) { 692 ex := c.OSCommand.GetLazygitPath() 693 694 debug := "FALSE" 695 if c.OSCommand.Config.GetDebug() == true { 696 debug = "TRUE" 697 } 698 699 splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha)) 700 701 cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...) 702 703 gitSequenceEditor := ex 704 if todo == "" { 705 gitSequenceEditor = "true" 706 } 707 708 cmd.Env = os.Environ() 709 cmd.Env = append( 710 cmd.Env, 711 "LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE", 712 "LAZYGIT_REBASE_TODO="+todo, 713 "DEBUG="+debug, 714 "LANG=en_US.UTF-8", // Force using EN as language 715 "LC_ALL=en_US.UTF-8", // Force using EN as language 716 "GIT_SEQUENCE_EDITOR="+gitSequenceEditor, 717 ) 718 719 if overrideEditor { 720 cmd.Env = append(cmd.Env, "EDITOR="+ex) 721 } 722 723 return cmd, nil 724 } 725 726 func (c *GitCommand) HardReset(baseSha string) error { 727 return c.OSCommand.RunCommand("git reset --hard " + baseSha) 728 } 729 730 func (c *GitCommand) SoftReset(baseSha string) error { 731 return c.OSCommand.RunCommand("git reset --soft " + baseSha) 732 } 733 734 func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, actionIndex int, action string) (string, string, error) { 735 baseIndex := actionIndex + 1 736 737 if len(commits) <= baseIndex { 738 return "", "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit")) 739 } 740 741 if action == "squash" || action == "fixup" { 742 baseIndex++ 743 744 if len(commits) <= baseIndex { 745 return "", "", errors.New(c.Tr.SLocalize("CannotSquashOntoSecondCommit")) 746 } 747 } 748 749 todo := "" 750 for i, commit := range commits[0:baseIndex] { 751 a := "pick" 752 if i == actionIndex { 753 a = action 754 } 755 todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo 756 } 757 758 return todo, commits[baseIndex].Sha, nil 759 } 760 761 // AmendTo amends the given commit with whatever files are staged 762 func (c *GitCommand) AmendTo(sha string) error { 763 if err := c.CreateFixupCommit(sha); err != nil { 764 return err 765 } 766 767 return c.SquashAllAboveFixupCommits(sha) 768 } 769 770 // EditRebaseTodo sets the action at a given index in the git-rebase-todo file 771 func (c *GitCommand) EditRebaseTodo(index int, action string) error { 772 fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir) 773 bytes, err := ioutil.ReadFile(fileName) 774 if err != nil { 775 return err 776 } 777 778 content := strings.Split(string(bytes), "\n") 779 commitCount := c.getTodoCommitCount(content) 780 781 // we have the most recent commit at the bottom whereas the todo file has 782 // it at the bottom, so we need to subtract our index from the commit count 783 contentIndex := commitCount - 1 - index 784 splitLine := strings.Split(content[contentIndex], " ") 785 content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ") 786 result := strings.Join(content, "\n") 787 788 return ioutil.WriteFile(fileName, []byte(result), 0644) 789 } 790 791 func (c *GitCommand) getTodoCommitCount(content []string) int { 792 // count lines that are not blank and are not comments 793 commitCount := 0 794 for _, line := range content { 795 if line != "" && !strings.HasPrefix(line, "#") { 796 commitCount++ 797 } 798 } 799 return commitCount 800 } 801 802 // MoveTodoDown moves a rebase todo item down by one position 803 func (c *GitCommand) MoveTodoDown(index int) error { 804 fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir) 805 bytes, err := ioutil.ReadFile(fileName) 806 if err != nil { 807 return err 808 } 809 810 content := strings.Split(string(bytes), "\n") 811 commitCount := c.getTodoCommitCount(content) 812 contentIndex := commitCount - 1 - index 813 814 rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1]) 815 rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...) 816 result := strings.Join(rearrangedContent, "\n") 817 818 return ioutil.WriteFile(fileName, []byte(result), 0644) 819 } 820 821 // Revert reverts the selected commit by sha 822 func (c *GitCommand) Revert(sha string) error { 823 return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha)) 824 } 825 826 // CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD 827 func (c *GitCommand) CherryPickCommits(commits []*Commit) error { 828 todo := "" 829 for _, commit := range commits { 830 todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo 831 } 832 833 cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) 834 if err != nil { 835 return err 836 } 837 838 return c.OSCommand.RunPreparedCommand(cmd) 839 } 840 841 // GetCommitFiles get the specified commit files 842 func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) { 843 cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitSha) 844 files, err := c.OSCommand.RunCommandWithOutput(cmd) 845 if err != nil { 846 return nil, err 847 } 848 849 commitFiles := make([]*CommitFile, 0) 850 851 for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") { 852 commitFiles = append(commitFiles, &CommitFile{ 853 Sha: commitSha, 854 Name: file, 855 DisplayString: file, 856 }) 857 } 858 859 return commitFiles, nil 860 } 861 862 // ShowCommitFile get the diff of specified commit file 863 func (c *GitCommand) ShowCommitFile(commitSha, fileName string) (string, error) { 864 cmd := fmt.Sprintf("git show --color %s -- %s", commitSha, fileName) 865 return c.OSCommand.RunCommandWithOutput(cmd) 866 } 867 868 // CheckoutFile checks out the file for the given commit 869 func (c *GitCommand) CheckoutFile(commitSha, fileName string) error { 870 cmd := fmt.Sprintf("git checkout %s %s", commitSha, fileName) 871 return c.OSCommand.RunCommand(cmd) 872 } 873 874 // DiscardOldFileChanges discards changes to a file from an old commit 875 func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error { 876 if len(commits)-1 < commitIndex { 877 return errors.New("index outside of range of commits") 878 } 879 880 // we can make this GPG thing possible it just means we need to do this in two parts: 881 // one where we handle the possibility of a credential request, and the other 882 // where we continue the rebase 883 if c.usingGpg() { 884 return errors.New(c.Tr.SLocalize("DisabledForGPG")) 885 } 886 887 todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit") 888 if err != nil { 889 return err 890 } 891 892 cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) 893 if err != nil { 894 return err 895 } 896 897 if err := c.OSCommand.RunPreparedCommand(cmd); err != nil { 898 return err 899 } 900 901 // check if file exists in previous commit (this command returns an error if the file doesn't exist) 902 if err := c.OSCommand.RunCommand(fmt.Sprintf("git cat-file -e HEAD^:%s", fileName)); err != nil { 903 if err := c.OSCommand.Remove(fileName); err != nil { 904 return err 905 } 906 if err := c.StageFile(fileName); err != nil { 907 return err 908 } 909 } else { 910 if err := c.CheckoutFile("HEAD^", fileName); err != nil { 911 return err 912 } 913 } 914 915 // amend the commit 916 cmd, err = c.AmendHead() 917 if cmd != nil { 918 return errors.New("received unexpected pointer to cmd") 919 } 920 if err != nil { 921 return err 922 } 923 924 // continue 925 return c.GenericMerge("rebase", "continue") 926 } 927 928 // DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .` 929 func (c *GitCommand) DiscardAnyUnstagedFileChanges() error { 930 return c.OSCommand.RunCommand("git checkout -- .") 931 } 932 933 // RemoveUntrackedFiles runs `git clean -fd` 934 func (c *GitCommand) RemoveUntrackedFiles() error { 935 return c.OSCommand.RunCommand("git clean -fd") 936 } 937 938 // ResetHardHead runs `git reset --hard HEAD` 939 func (c *GitCommand) ResetHardHead() error { 940 return c.OSCommand.RunCommand("git reset --hard HEAD") 941 } 942 943 // ResetSoftHead runs `git reset --soft HEAD` 944 func (c *GitCommand) ResetSoftHead() error { 945 return c.OSCommand.RunCommand("git reset --soft HEAD") 946 } 947 948 // DiffCommits show diff between commits 949 func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) { 950 cmd := fmt.Sprintf("git diff --color %s %s", sha1, sha2) 951 return c.OSCommand.RunCommandWithOutput(cmd) 952 } 953 954 // CreateFixupCommit creates a commit that fixes up a previous commit 955 func (c *GitCommand) CreateFixupCommit(sha string) error { 956 cmd := fmt.Sprintf("git commit --fixup=%s", sha) 957 return c.OSCommand.RunCommand(cmd) 958 } 959 960 // SquashAllAboveFixupCommits squashes all fixup! commits above the given one 961 func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error { 962 return c.RunSkipEditorCommand( 963 fmt.Sprintf( 964 "git rebase --interactive --autostash --autosquash %s^", 965 sha, 966 ), 967 ) 968 } 969 970 // StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps 971 // shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible 972 func (c *GitCommand) StashSaveStagedChanges(message string) error { 973 974 if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil { 975 return err 976 } 977 978 if err := c.StashSave(message); err != nil { 979 return err 980 } 981 982 if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil { 983 return err 984 } 985 986 if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil { 987 return err 988 } 989 990 if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil { 991 return err 992 } 993 994 // if you had staged an untracked file, that will now appear as 'AD' in git status 995 // meaning it's deleted in your working tree but added in your index. Given that it's 996 // now safely stashed, we need to remove it. 997 files := c.GetStatusFiles() 998 for _, file := range files { 999 if file.ShortStatus == "AD" { 1000 if err := c.UnStageFile(file.Name, false); err != nil { 1001 return err 1002 } 1003 } 1004 } 1005 1006 return nil 1007 }