1package commands 2 3import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 11 "github.com/go-errors/errors" 12 "github.com/jesseduffield/lazygit/pkg/commands/models" 13 "github.com/mgutz/str" 14) 15 16func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) { 17 todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword") 18 if err != nil { 19 return nil, err 20 } 21 22 return c.PrepareInteractiveRebaseCommand(sha, todo, false) 23} 24 25func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error { 26 // we must ensure that we have at least two commits after the selected one 27 if len(commits) <= index+2 { 28 // assuming they aren't picking the bottom commit 29 return errors.New(c.Tr.NoRoom) 30 } 31 32 todo := "" 33 orderedCommits := append(commits[0:index], commits[index+1], commits[index]) 34 for _, commit := range orderedCommits { 35 todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo 36 } 37 38 cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) 39 if err != nil { 40 return err 41 } 42 43 return c.OSCommand.RunPreparedCommand(cmd) 44} 45 46func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error { 47 todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action) 48 if err != nil { 49 return err 50 } 51 52 cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) 53 if err != nil { 54 return err 55 } 56 57 return c.OSCommand.RunPreparedCommand(cmd) 58} 59 60// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase 61// we tell git to run lazygit to edit the todo list, and we pass the client 62// lazygit a todo string to write to the todo file 63func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) { 64 ex := c.OSCommand.GetLazygitPath() 65 66 debug := "FALSE" 67 if c.OSCommand.Config.GetDebug() { 68 debug = "TRUE" 69 } 70 71 cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha) 72 c.Log.WithField("command", cmdStr).Info("RunCommand") 73 splitCmd := str.ToArgv(cmdStr) 74 75 cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...) 76 77 gitSequenceEditor := ex 78 if todo == "" { 79 gitSequenceEditor = "true" 80 } else { 81 c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false) 82 } 83 84 cmd.Env = os.Environ() 85 cmd.Env = append( 86 cmd.Env, 87 "LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE", 88 "LAZYGIT_REBASE_TODO="+todo, 89 "DEBUG="+debug, 90 "LANG=en_US.UTF-8", // Force using EN as language 91 "LC_ALL=en_US.UTF-8", // Force using EN as language 92 "GIT_SEQUENCE_EDITOR="+gitSequenceEditor, 93 ) 94 95 if overrideEditor { 96 cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex) 97 } 98 99 return cmd, nil 100} 101 102func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) { 103 baseIndex := actionIndex + 1 104 105 if len(commits) <= baseIndex { 106 return "", "", errors.New(c.Tr.CannotRebaseOntoFirstCommit) 107 } 108 109 if action == "squash" || action == "fixup" { 110 baseIndex++ 111 112 if len(commits) <= baseIndex { 113 return "", "", errors.New(c.Tr.CannotSquashOntoSecondCommit) 114 } 115 } 116 117 todo := "" 118 for i, commit := range commits[0:baseIndex] { 119 var commitAction string 120 if i == actionIndex { 121 commitAction = action 122 } else if commit.IsMerge() { 123 // your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary! 124 // doing this means we don't need to worry about rebasing over merges which always causes problems. 125 // you typically shouldn't be doing rebases that pass over merge commits anyway. 126 commitAction = "drop" 127 } else { 128 commitAction = "pick" 129 } 130 todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo 131 } 132 133 return todo, commits[baseIndex].Sha, nil 134} 135 136// AmendTo amends the given commit with whatever files are staged 137func (c *GitCommand) AmendTo(sha string) error { 138 if err := c.CreateFixupCommit(sha); err != nil { 139 return err 140 } 141 142 return c.SquashAllAboveFixupCommits(sha) 143} 144 145// EditRebaseTodo sets the action at a given index in the git-rebase-todo file 146func (c *GitCommand) EditRebaseTodo(index int, action string) error { 147 fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") 148 bytes, err := ioutil.ReadFile(fileName) 149 if err != nil { 150 return err 151 } 152 153 content := strings.Split(string(bytes), "\n") 154 commitCount := c.getTodoCommitCount(content) 155 156 // we have the most recent commit at the bottom whereas the todo file has 157 // it at the bottom, so we need to subtract our index from the commit count 158 contentIndex := commitCount - 1 - index 159 splitLine := strings.Split(content[contentIndex], " ") 160 content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ") 161 result := strings.Join(content, "\n") 162 163 return ioutil.WriteFile(fileName, []byte(result), 0644) 164} 165 166func (c *GitCommand) getTodoCommitCount(content []string) int { 167 // count lines that are not blank and are not comments 168 commitCount := 0 169 for _, line := range content { 170 if line != "" && !strings.HasPrefix(line, "#") { 171 commitCount++ 172 } 173 } 174 return commitCount 175} 176 177// MoveTodoDown moves a rebase todo item down by one position 178func (c *GitCommand) MoveTodoDown(index int) error { 179 fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") 180 bytes, err := ioutil.ReadFile(fileName) 181 if err != nil { 182 return err 183 } 184 185 content := strings.Split(string(bytes), "\n") 186 commitCount := c.getTodoCommitCount(content) 187 contentIndex := commitCount - 1 - index 188 189 rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1]) 190 rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...) 191 result := strings.Join(rearrangedContent, "\n") 192 193 return ioutil.WriteFile(fileName, []byte(result), 0644) 194} 195 196// SquashAllAboveFixupCommits squashes all fixup! commits above the given one 197func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error { 198 return c.runSkipEditorCommand( 199 fmt.Sprintf( 200 "git rebase --interactive --autostash --autosquash %s^", 201 sha, 202 ), 203 ) 204} 205 206// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current 207// commit and pick all others. After this you'll want to call `c.GenericMergeOrRebaseAction("rebase", "continue")` 208func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error { 209 if len(commits)-1 < commitIndex { 210 return errors.New("index outside of range of commits") 211 } 212 213 // we can make this GPG thing possible it just means we need to do this in two parts: 214 // one where we handle the possibility of a credential request, and the other 215 // where we continue the rebase 216 if c.UsingGpg() { 217 return errors.New(c.Tr.DisabledForGPG) 218 } 219 220 todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit") 221 if err != nil { 222 return err 223 } 224 225 cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) 226 if err != nil { 227 return err 228 } 229 230 if err := c.OSCommand.RunPreparedCommand(cmd); err != nil { 231 return err 232 } 233 234 return nil 235} 236 237// RebaseBranch interactive rebases onto a branch 238func (c *GitCommand) RebaseBranch(branchName string) error { 239 cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) 240 if err != nil { 241 return err 242 } 243 244 return c.OSCommand.RunPreparedCommand(cmd) 245} 246 247// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" 248// By default we skip the editor in the case where a commit will be made 249func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command string) error { 250 err := c.runSkipEditorCommand( 251 fmt.Sprintf( 252 "git %s --%s", 253 commandType, 254 command, 255 ), 256 ) 257 if err != nil { 258 if !strings.Contains(err.Error(), "no rebase in progress") { 259 return err 260 } 261 c.Log.Warn(err) 262 } 263 264 // sometimes we need to do a sequence of things in a rebase but the user needs to 265 // fix merge conflicts along the way. When this happens we queue up the next step 266 // so that after the next successful rebase continue we can continue from where we left off 267 if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil { 268 f := c.onSuccessfulContinue 269 c.onSuccessfulContinue = nil 270 return f() 271 } 272 if command == "abort" { 273 c.onSuccessfulContinue = nil 274 } 275 return nil 276} 277 278func (c *GitCommand) runSkipEditorCommand(command string) error { 279 cmd := c.OSCommand.ExecutableFromString(command) 280 lazyGitPath := c.OSCommand.GetLazygitPath() 281 cmd.Env = append( 282 cmd.Env, 283 "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY", 284 "GIT_EDITOR="+lazyGitPath, 285 "EDITOR="+lazyGitPath, 286 "VISUAL="+lazyGitPath, 287 ) 288 return c.OSCommand.RunExecutable(cmd) 289} 290