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