1package git
2
3import (
4	"fmt"
5	"io/ioutil"
6	"os"
7	"path/filepath"
8	"regexp"
9	"sort"
10	"strconv"
11	"strings"
12
13	"github.com/git-town/git-town/src/config"
14	"github.com/git-town/git-town/src/run"
15	"github.com/git-town/git-town/src/stringslice"
16)
17
18// Runner executes Git commands.
19type Runner struct {
20	run.Shell                            // for running console commands
21	Config             *config.Config    // caches Git configuration settings
22	CurrentBranchCache *StringCache      // caches the currently checked out Git branch
23	DryRun             *DryRun           // tracks dry-run information
24	IsRepoCache        *BoolCache        // caches whether the current directory is a Git repo
25	RemoteBranchCache  *StringSliceCache // caches the remote branches of this Git repo
26	RemotesCache       *StringSliceCache // caches Git remotes
27	RootDirCache       *StringCache      // caches the base of the Git directory
28}
29
30// AbortMerge cancels a currently ongoing Git merge operation.
31func (r *Runner) AbortMerge() error {
32	_, err := r.Run("git", "merge", "--abort")
33	if err != nil {
34		return fmt.Errorf("cannot abort current merge: %w", err)
35	}
36	return nil
37}
38
39// AbortRebase cancels a currently ongoing Git rebase operation.
40func (r *Runner) AbortRebase() error {
41	_, err := r.Run("git", "rebase", "--abort")
42	if err != nil {
43		return fmt.Errorf("cannot abort current merge: %w", err)
44	}
45	return nil
46}
47
48// AddRemote adds the given Git remote to this repository.
49func (r *Runner) AddRemote(name, value string) error {
50	_, err := r.Run("git", "remote", "add", name, value)
51	if err != nil {
52		return fmt.Errorf("cannot add remote %q --> %q: %w", name, value, err)
53	}
54	r.RemotesCache.Invalidate()
55	return nil
56}
57
58// Author returns the locally Git configured user.
59func (r *Runner) Author() (author string, err error) {
60	out, err := r.Run("git", "config", "user.name")
61	if err != nil {
62		return "", err
63	}
64	name := out.OutputSanitized()
65	out, err = r.Run("git", "config", "user.email")
66	if err != nil {
67		return "", err
68	}
69	email := out.OutputSanitized()
70	return name + " <" + email + ">", nil
71}
72
73// BranchHasUnmergedCommits indicates whether the branch with the given name
74// contains commits that are not merged into the main branch.
75func (r *Runner) BranchHasUnmergedCommits(branch string) (bool, error) {
76	out, err := r.Run("git", "log", r.Config.GetMainBranch()+".."+branch)
77	if err != nil {
78		return false, fmt.Errorf("cannot determine if branch %q has unmerged commits: %w", branch, err)
79	}
80	return out.OutputSanitized() != "", nil
81}
82
83// CheckoutBranch checks out the Git branch with the given name in this repo.
84func (r *Runner) CheckoutBranch(name string) error {
85	_, err := r.Run("git", "checkout", name)
86	if err != nil {
87		return fmt.Errorf("cannot check out branch %q in repo %q: %w", name, r.WorkingDir(), err)
88	}
89	if name != "-" {
90		r.CurrentBranchCache.Set(name)
91	} else {
92		r.CurrentBranchCache.Invalidate()
93	}
94	return nil
95}
96
97// CommentOutSquashCommitMessage comments out the message for the current squash merge
98// Adds the given prefix with the newline if provided.
99func (r *Runner) CommentOutSquashCommitMessage(prefix string) error {
100	squashMessageFile := ".git/SQUASH_MSG"
101	contentBytes, err := ioutil.ReadFile(squashMessageFile)
102	if err != nil {
103		return fmt.Errorf("cannot read squash message file %q: %w", squashMessageFile, err)
104	}
105	content := string(contentBytes)
106	if prefix != "" {
107		content = prefix + "\n" + content
108	}
109	content = regexp.MustCompile("(?m)^").ReplaceAllString(content, "# ")
110	return ioutil.WriteFile(squashMessageFile, []byte(content), 0600)
111}
112
113// CommitNoEdit commits all staged files with the default commit message.
114func (r *Runner) CommitNoEdit() error {
115	_, err := r.Run("git", "commit", "--no-edit")
116	if err != nil {
117		return fmt.Errorf("cannot commit files: %w", err)
118	}
119	return nil
120}
121
122// Commits provides a list of the commits in this Git repository with the given fields.
123func (r *Runner) Commits(fields []string) (result []Commit, err error) {
124	branches, err := r.LocalBranchesMainFirst()
125	if err != nil {
126		return result, fmt.Errorf("cannot determine the Git branches: %w", err)
127	}
128	for _, branch := range branches {
129		commits, err := r.CommitsInBranch(branch, fields)
130		if err != nil {
131			return result, err
132		}
133		result = append(result, commits...)
134	}
135	return result, nil
136}
137
138// CommitsInBranch provides all commits in the given Git branch.
139func (r *Runner) CommitsInBranch(branch string, fields []string) (result []Commit, err error) {
140	outcome, err := r.Run("git", "log", branch, "--format=%h|%s|%an <%ae>", "--topo-order", "--reverse")
141	if err != nil {
142		return result, fmt.Errorf("cannot get commits in branch %q: %w", branch, err)
143	}
144	for _, line := range strings.Split(outcome.OutputSanitized(), "\n") {
145		parts := strings.Split(line, "|")
146		commit := Commit{Branch: branch, SHA: parts[0], Message: parts[1], Author: parts[2]}
147		if strings.EqualFold(commit.Message, "initial commit") {
148			continue
149		}
150		if stringslice.Contains(fields, "FILE NAME") {
151			filenames, err := r.FilesInCommit(commit.SHA)
152			if err != nil {
153				return result, fmt.Errorf("cannot determine file name for commit %q in branch %q: %w", commit.SHA, branch, err)
154			}
155			commit.FileName = strings.Join(filenames, ", ")
156		}
157		if stringslice.Contains(fields, "FILE CONTENT") {
158			filecontent, err := r.FileContentInCommit(commit.SHA, commit.FileName)
159			if err != nil {
160				return result, fmt.Errorf("cannot determine file content for commit %q in branch %q: %w", commit.SHA, branch, err)
161			}
162			commit.FileContent = filecontent
163		}
164		result = append(result, commit)
165	}
166	return result, nil
167}
168
169// CommitStagedChanges commits the currently staged changes.
170func (r *Runner) CommitStagedChanges(message string) error {
171	var err error
172	if message != "" {
173		_, err = r.Run("git", "commit", "-m", message)
174	} else {
175		_, err = r.Run("git", "commit", "--no-edit")
176	}
177	if err != nil {
178		return fmt.Errorf("cannot commit staged changes: %w", err)
179	}
180	return nil
181}
182
183// CommitWithMessageAndAuthor .
184func (r *Runner) CommitWithMessageAndAuthor(message, author string) error {
185	_, err := r.Run("git", "commit", "-m", message, "--author", author)
186	if err != nil {
187		return fmt.Errorf("cannot commit with message %q and author %q: %w", message, author, err)
188	}
189	return nil
190}
191
192// CommitWithMessage commits the staged changes with the given commit message.
193func (r *Runner) CommitWithMessage(message string) error {
194	_, err := r.Run("git", "commit", "-m", message)
195	if err != nil {
196		return fmt.Errorf("cannot commit with message %q: %w", message, err)
197	}
198	return nil
199}
200
201// Commit .
202func (r *Runner) Commit() error {
203	_, err := r.Run("git", "commit")
204	return err
205}
206
207// ConnectTrackingBranch connects the branch with the given name to its remote tracking branch.
208// The branch must exist.
209func (r *Runner) ConnectTrackingBranch(name string) error {
210	_, err := r.Run("git", "branch", "--set-upstream-to=origin/"+name, name)
211	if err != nil {
212		return fmt.Errorf("cannot connect tracking branch for %q: %w", name, err)
213	}
214	return nil
215}
216
217// ContinueRebase continues the currently ongoing rebase.
218func (r *Runner) ContinueRebase() error {
219	_, err := r.Run("git", "rebase", "--continue")
220	if err != nil {
221		return fmt.Errorf("cannot continue rebase: %w", err)
222	}
223	return nil
224}
225
226// CreateBranch creates a new branch with the given name.
227// The created branch is a normal branch.
228// To create feature branches, use CreateFeatureBranch.
229func (r *Runner) CreateBranch(name, parent string) error {
230	_, err := r.Run("git", "branch", name, parent)
231	if err != nil {
232		return fmt.Errorf("cannot create branch %q: %w", name, err)
233	}
234	return nil
235}
236
237// CreateChildFeatureBranch creates a branch with the given name and parent in this repository.
238// The parent branch must already exist.
239func (r *Runner) CreateChildFeatureBranch(name string, parent string) error {
240	err := r.CreateBranch(name, parent)
241	if err != nil {
242		return fmt.Errorf("cannot create child branch %q: %w", name, err)
243	}
244	_ = r.Config.SetParentBranch(name, parent)
245	return nil
246}
247
248// CreateCommit creates a commit with the given properties in this Git repo.
249func (r *Runner) CreateCommit(commit Commit) error {
250	err := r.CheckoutBranch(commit.Branch)
251	if err != nil {
252		return fmt.Errorf("cannot checkout branch %q: %w", commit.Branch, err)
253	}
254	err = r.CreateFile(commit.FileName, commit.FileContent)
255	if err != nil {
256		return fmt.Errorf("cannot create file %q needed for commit: %w", commit.FileName, err)
257	}
258	_, err = r.Run("git", "add", commit.FileName)
259	if err != nil {
260		return fmt.Errorf("cannot add file to commit: %w", err)
261	}
262	commands := []string{"commit", "-m", commit.Message}
263	if commit.Author != "" {
264		commands = append(commands, "--author="+commit.Author)
265	}
266	_, err = r.Run("git", commands...)
267	if err != nil {
268		return fmt.Errorf("cannot commit: %w", err)
269	}
270	return nil
271}
272
273// CreateFeatureBranch creates a feature branch with the given name in this repository.
274func (r *Runner) CreateFeatureBranch(name string) error {
275	err := r.RunMany([][]string{
276		{"git", "branch", name, "main"},
277		{"git", "config", "git-town-branch." + name + ".parent", "main"},
278	})
279	if err != nil {
280		return fmt.Errorf("cannot create feature branch %q: %w", name, err)
281	}
282	return nil
283}
284
285// CreateFeatureBranchNoParent creates a feature branch with no defined parent in this repository.
286func (r *Runner) CreateFeatureBranchNoParent(name string) error {
287	_, err := r.Run("git", "branch", name, "main")
288	if err != nil {
289		return fmt.Errorf("cannot create feature branch %q: %w", name, err)
290	}
291	return nil
292}
293
294// CreateFile creates a file with the given name and content in this repository.
295func (r *Runner) CreateFile(name, content string) error {
296	filePath := filepath.Join(r.WorkingDir(), name)
297	folderPath := filepath.Dir(filePath)
298	err := os.MkdirAll(folderPath, os.ModePerm)
299	if err != nil {
300		return fmt.Errorf("cannot create folder %q: %v", folderPath, err)
301	}
302	err = ioutil.WriteFile(filePath, []byte(content), 0500)
303	if err != nil {
304		return fmt.Errorf("cannot create file %q: %w", name, err)
305	}
306	return nil
307}
308
309// CreatePerennialBranches creates perennial branches with the given names in this repository.
310func (r *Runner) CreatePerennialBranches(names ...string) error {
311	for _, name := range names {
312		err := r.CreateBranch(name, "main")
313		if err != nil {
314			return fmt.Errorf("cannot create perennial branch %q in repo %q: %w", name, r.WorkingDir(), err)
315		}
316	}
317	return r.Config.AddToPerennialBranches(names...)
318}
319
320// CreateRemoteBranch creates a remote branch from the given local SHA.
321func (r *Runner) CreateRemoteBranch(localSha, branchName string) error {
322	_, err := r.Run("git", "push", "origin", localSha+":refs/heads/"+branchName)
323	if err != nil {
324		return fmt.Errorf("cannot create remote branch for local SHA %q: %w", localSha, err)
325	}
326	return nil
327}
328
329// CreateStandaloneTag creates a tag not on a branch.
330func (r *Runner) CreateStandaloneTag(name string) error {
331	return r.RunMany([][]string{
332		{"git", "checkout", "-b", "temp"},
333		{"touch", "a.txt"},
334		{"git", "add", "-A"},
335		{"git", "commit", "-m", "temp"},
336		{"git", "tag", "-a", name, "-m", name},
337		{"git", "checkout", "-"},
338		{"git", "branch", "-D", "temp"},
339	})
340}
341
342// CreateTag creates a tag with the given name.
343func (r *Runner) CreateTag(name string) error {
344	_, err := r.Run("git", "tag", "-a", name, "-m", name)
345	return err
346}
347
348// CreateTrackingBranch creates a remote tracking branch for the given local branch.
349func (r *Runner) CreateTrackingBranch(branch string) error {
350	_, err := r.Run("git", "push", "-u", "origin", branch)
351	if err != nil {
352		return fmt.Errorf("cannot create tracking branch for %q: %w", branch, err)
353	}
354	return nil
355}
356
357// CurrentBranch provides the currently checked out branch for this repo.
358func (r *Runner) CurrentBranch() (result string, err error) {
359	if r.DryRun.IsActive() {
360		return r.DryRun.CurrentBranch(), nil
361	}
362	if r.CurrentBranchCache.Initialized() {
363		return r.CurrentBranchCache.Value(), nil
364	}
365	rebasing, err := r.HasRebaseInProgress()
366	if err != nil {
367		return "", fmt.Errorf("cannot determine current branch: %w", err)
368	}
369	if rebasing {
370		currentBranch, err := r.currentBranchDuringRebase()
371		if err != nil {
372			return "", err
373		}
374		r.CurrentBranchCache.Set(currentBranch)
375		return currentBranch, nil
376	}
377	outcome, err := r.Run("git", "rev-parse", "--abbrev-ref", "HEAD")
378	if err != nil {
379		return "", fmt.Errorf("cannot determine the current branch: %w", err)
380	}
381	r.CurrentBranchCache.Set(outcome.OutputSanitized())
382	return r.CurrentBranchCache.Value(), nil
383}
384
385func (r *Runner) currentBranchDuringRebase() (string, error) {
386	rootDir, err := r.RootDirectory()
387	if err != nil {
388		return "", err
389	}
390	rawContent, err := ioutil.ReadFile(fmt.Sprintf("%s/.git/rebase-apply/head-name", rootDir))
391	if err != nil {
392		// Git 2.26 introduces a new rebase backend, see https://github.com/git/git/blob/master/Documentation/RelNotes/2.26.0.txt
393		rawContent, err = ioutil.ReadFile(fmt.Sprintf("%s/.git/rebase-merge/head-name", rootDir))
394		if err != nil {
395			return "", err
396		}
397	}
398	content := strings.TrimSpace(string(rawContent))
399	return strings.Replace(content, "refs/heads/", "", -1), nil
400}
401
402// CurrentSha provides the SHA of the currently checked out branch/commit.
403func (r *Runner) CurrentSha() (string, error) {
404	return r.ShaForBranch("HEAD")
405}
406
407// DeleteLastCommit resets HEAD to the previous commit.
408func (r *Runner) DeleteLastCommit() error {
409	_, err := r.Run("git", "reset", "--hard", "HEAD~1")
410	if err != nil {
411		return fmt.Errorf("cannot delete last commit: %w", err)
412	}
413	return nil
414}
415
416// DeleteLocalBranch removes the local branch with the given name.
417func (r *Runner) DeleteLocalBranch(name string, force bool) error {
418	args := []string{"branch", "-d", name}
419	if force {
420		args[1] = "-D"
421	}
422	_, err := r.Run("git", args...)
423	if err != nil {
424		return fmt.Errorf("cannot delete local branch %q: %w", name, err)
425	}
426	return nil
427}
428
429// DeleteMainBranchConfiguration removes the configuration for which branch is the main branch.
430func (r *Runner) DeleteMainBranchConfiguration() error {
431	_, err := r.Run("git", "config", "--unset", "git-town.main-branch-name")
432	if err != nil {
433		return fmt.Errorf("cannot delete main branch configuration: %w", err)
434	}
435	return nil
436}
437
438// DeleteRemoteBranch removes the remote branch of the given local branch.
439func (r *Runner) DeleteRemoteBranch(name string) error {
440	_, err := r.Run("git", "push", "origin", ":"+name)
441	if err != nil {
442		return fmt.Errorf("cannot delete tracking branch for %q: %w", name, err)
443	}
444	return nil
445}
446
447// DiffParent displays the diff between the given branch and its given parent branch.
448func (r *Runner) DiffParent(branch, parentBranch string) error {
449	_, err := r.Run("git", "diff", parentBranch+".."+branch)
450	if err != nil {
451		return fmt.Errorf("cannot diff branch %q with its parent branch %q: %w", branch, parentBranch, err)
452	}
453	return nil
454}
455
456// DiscardOpenChanges deletes all uncommitted changes.
457func (r *Runner) DiscardOpenChanges() error {
458	_, err := r.Run("git", "reset", "--hard")
459	if err != nil {
460		return fmt.Errorf("cannot discard open changes: %w", err)
461	}
462	return nil
463}
464
465// ExpectedPreviouslyCheckedOutBranch returns what is the expected previously checked out branch
466// given the inputs.
467func (r *Runner) ExpectedPreviouslyCheckedOutBranch(initialPreviouslyCheckedOutBranch, initialBranch string) (string, error) {
468	hasInitialPreviouslyCheckedOutBranch, err := r.HasLocalBranch(initialPreviouslyCheckedOutBranch)
469	if err != nil {
470		return "", err
471	}
472	if hasInitialPreviouslyCheckedOutBranch {
473		currentBranch, err := r.CurrentBranch()
474		if err != nil {
475			return "", err
476		}
477		hasInitialBranch, err := r.HasLocalBranch(initialBranch)
478		if err != nil {
479			return "", err
480		}
481		if currentBranch == initialBranch || !hasInitialBranch {
482			return initialPreviouslyCheckedOutBranch, nil
483		}
484		return initialBranch, nil
485	}
486	return r.Config.GetMainBranch(), nil
487}
488
489// Fetch retrieves the updates from the remote repo.
490func (r *Runner) Fetch() error {
491	_, err := r.Run("git", "fetch", "--prune", "--tags")
492	if err != nil {
493		return fmt.Errorf("cannot fetch: %w", err)
494	}
495	return nil
496}
497
498// FetchUpstream fetches updates from the upstream remote.
499func (r *Runner) FetchUpstream(branch string) error {
500	_, err := r.Run("git", "fetch", "upstream", branch)
501	if err != nil {
502		return fmt.Errorf("cannot fetch from upstream: %w", err)
503	}
504	return nil
505}
506
507// FileContent provides the current content of a file.
508func (r *Runner) FileContent(filename string) (result string, err error) {
509	content, err := ioutil.ReadFile(filepath.Join(r.WorkingDir(), filename))
510	return string(content), err
511}
512
513// FileContentInCommit provides the content of the file with the given name in the commit with the given SHA.
514func (r *Runner) FileContentInCommit(sha string, filename string) (result string, err error) {
515	outcome, err := r.Run("git", "show", sha+":"+filename)
516	if err != nil {
517		return result, fmt.Errorf("cannot determine the content for file %q in commit %q: %w", filename, sha, err)
518	}
519	return outcome.OutputSanitized(), nil
520}
521
522// FilesInCommit provides the names of the files that the commit with the given SHA changes.
523func (r *Runner) FilesInCommit(sha string) (result []string, err error) {
524	outcome, err := r.Run("git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha)
525	if err != nil {
526		return result, fmt.Errorf("cannot get files for commit %q: %w", sha, err)
527	}
528	return strings.Split(outcome.OutputSanitized(), "\n"), nil
529}
530
531// FilesInBranch provides the list of the files present in the given branch.
532func (r *Runner) FilesInBranch(branch string) (result []string, err error) {
533	outcome, err := r.Run("git", "ls-tree", "-r", "--name-only", branch)
534	if err != nil {
535		return result, fmt.Errorf("cannot determine files in branch %q in repo %q: %w", branch, r.WorkingDir(), err)
536	}
537	for _, line := range strings.Split(outcome.OutputSanitized(), "\n") {
538		file := strings.TrimSpace(line)
539		if file != "" {
540			result = append(result, file)
541		}
542	}
543	return result, err
544}
545
546// HasBranchesOutOfSync indicates whether one or more local branches are out of sync with their remote.
547func (r *Runner) HasBranchesOutOfSync() (bool, error) {
548	res, err := r.Run("git", "for-each-ref", "--format=%(refname:short) %(upstream:track)", "refs/heads")
549	if err != nil {
550		return false, fmt.Errorf("cannot determine if branches are out of sync in %q: %w %q", r.WorkingDir(), err, res.Output())
551	}
552	return strings.Contains(res.Output(), "["), nil
553}
554
555// HasConflicts returns whether the local repository currently has unresolved merge conflicts.
556func (r *Runner) HasConflicts() (bool, error) {
557	res, err := r.Run("git", "status")
558	if err != nil {
559		return false, fmt.Errorf("cannot determine conflicts: %w", err)
560	}
561	return res.OutputContainsText("Unmerged paths"), nil
562}
563
564// HasFile indicates whether this repository contains a file with the given name and content.
565func (r *Runner) HasFile(name, content string) (result bool, err error) {
566	rawContent, err := ioutil.ReadFile(filepath.Join(r.WorkingDir(), name))
567	if err != nil {
568		return result, fmt.Errorf("repo doesn't have file %q: %w", name, err)
569	}
570	actualContent := string(rawContent)
571	if actualContent != content {
572		return result, fmt.Errorf("file %q should have content %q but has %q", name, content, actualContent)
573	}
574	return true, nil
575}
576
577// HasGitTownConfigNow indicates whether this repository contain Git Town specific configuration.
578func (r *Runner) HasGitTownConfigNow() (result bool, err error) {
579	outcome, err := r.Run("git", "config", "--local", "--get-regex", "git-town")
580	if outcome.ExitCode() == 1 {
581		return false, nil
582	}
583	return outcome.OutputSanitized() != "", err
584}
585
586// HasLocalBranch indicates whether this repo has a local branch with the given name.
587func (r *Runner) HasLocalBranch(name string) (bool, error) {
588	branches, err := r.LocalBranchesMainFirst()
589	if err != nil {
590		return false, fmt.Errorf("cannot determine whether the local branch %q exists: %w", name, err)
591	}
592	return stringslice.Contains(branches, name), nil
593}
594
595// HasLocalOrRemoteBranch indicates whether this repo has a local or remote branch with the given name.
596func (r *Runner) HasLocalOrRemoteBranch(name string) (bool, error) {
597	branches, err := r.LocalAndRemoteBranches()
598	if err != nil {
599		return false, fmt.Errorf("cannot determine whether the local or remote branch %q exists: %w", name, err)
600	}
601	return stringslice.Contains(branches, name), nil
602}
603
604// HasMergeInProgress indicates whether this Git repository currently has a merge in progress.
605func (r *Runner) HasMergeInProgress() (result bool, err error) {
606	_, err = os.Stat(filepath.Join(r.WorkingDir(), ".git", "MERGE_HEAD"))
607	return err == nil, nil
608}
609
610// HasOpenChanges indicates whether this repo has open changes.
611func (r *Runner) HasOpenChanges() (bool, error) {
612	outcome, err := r.Run("git", "status", "--porcelain")
613	if err != nil {
614		return false, fmt.Errorf("cannot determine open changes: %w", err)
615	}
616	return outcome.OutputSanitized() != "", nil
617}
618
619// HasRebaseInProgress indicates whether this Git repository currently has a rebase in progress.
620func (r *Runner) HasRebaseInProgress() (bool, error) {
621	res, err := r.Run("git", "status")
622	if err != nil {
623		return false, fmt.Errorf("cannot determine rebase in %q progress: %w", r.WorkingDir(), err)
624	}
625	output := res.OutputSanitized()
626	if strings.Contains(output, "You are currently rebasing") {
627		return true, nil
628	}
629	if strings.Contains(output, "rebase in progress") {
630		return true, nil
631	}
632	return false, nil
633}
634
635// HasRemote indicates whether this repo has a remote with the given name.
636func (r *Runner) HasRemote(name string) (result bool, err error) {
637	remotes, err := r.Remotes()
638	if err != nil {
639		return false, fmt.Errorf("cannot determine if remote %q exists: %w", name, err)
640	}
641	return stringslice.Contains(remotes, name), nil
642}
643
644// HasShippableChanges indicates whether the given branch has changes
645// not currently in the main branch.
646func (r *Runner) HasShippableChanges(branch string) (bool, error) {
647	out, err := r.Run("git", "diff", r.Config.GetMainBranch()+".."+branch)
648	if err != nil {
649		return false, fmt.Errorf("cannot determine whether branch %q has shippable changes: %w", branch, err)
650	}
651	return out.OutputSanitized() != "", nil
652}
653
654// HasTrackingBranch indicates whether the local branch with the given name has a remote tracking branch.
655func (r *Runner) HasTrackingBranch(name string) (result bool, err error) {
656	trackingBranchName := "origin/" + name
657	remoteBranches, err := r.RemoteBranches()
658	if err != nil {
659		return false, fmt.Errorf("cannot determine if tracking branch %q exists: %w", name, err)
660	}
661	for _, line := range remoteBranches {
662		if strings.TrimSpace(line) == trackingBranchName {
663			return true, nil
664		}
665	}
666	return false, nil
667}
668
669// IsBranchInSync returns whether the branch with the given name is in sync with its tracking branch.
670func (r *Runner) IsBranchInSync(branchName string) (bool, error) {
671	hasTrackingBranch, err := r.HasTrackingBranch(branchName)
672	if err != nil {
673		return false, err
674	}
675	if hasTrackingBranch {
676		localSha, err := r.ShaForBranch(branchName)
677		if err != nil {
678			return false, err
679		}
680		remoteSha, err := r.ShaForBranch(r.TrackingBranchName(branchName))
681		return localSha == remoteSha, err
682	}
683	return true, nil
684}
685
686// IsRepository returns whether or not the current directory is in a repository.
687func (r *Runner) IsRepository() bool {
688	if !r.IsRepoCache.Initialized() {
689		_, err := run.Exec("git", "rev-parse")
690		r.IsRepoCache.Set(err == nil)
691	}
692	return r.IsRepoCache.Value()
693}
694
695// LastCommitMessage returns the commit message for the last commit.
696func (r *Runner) LastCommitMessage() (string, error) {
697	out, err := r.Run("git", "log", "-1", "--format=%B")
698	if err != nil {
699		return "", fmt.Errorf("cannot determine last commit message: %w", err)
700	}
701	return out.OutputSanitized(), nil
702}
703
704// LocalAndRemoteBranches provides the names of all local branches in this repo.
705func (r *Runner) LocalAndRemoteBranches() ([]string, error) {
706	outcome, err := r.Run("git", "branch", "-a")
707	if err != nil {
708		return []string{}, fmt.Errorf("cannot determine the local branches")
709	}
710	lines := outcome.OutputLines()
711	branchNames := make(map[string]struct{})
712	for l := range lines {
713		if !strings.Contains(lines[l], " -> ") {
714			branchNames[strings.TrimSpace(strings.Replace(strings.Replace(lines[l], "* ", "", 1), "remotes/origin/", "", 1))] = struct{}{}
715		}
716	}
717	result := make([]string, len(branchNames))
718	i := 0
719	for branchName := range branchNames {
720		result[i] = branchName
721		i++
722	}
723	sort.Strings(result)
724	return MainFirst(result), nil
725}
726
727// LocalBranches returns the names of all branches in the local repository,
728// ordered alphabetically.
729func (r *Runner) LocalBranches() (result []string, err error) {
730	res, err := r.Run("git", "branch")
731	if err != nil {
732		return result, err
733	}
734	for _, line := range res.OutputLines() {
735		line = strings.Trim(line, "* ")
736		line = strings.TrimSpace(line)
737		result = append(result, line)
738	}
739	return
740}
741
742// LocalBranchesMainFirst provides the names of all local branches in this repo.
743func (r *Runner) LocalBranchesMainFirst() (result []string, err error) {
744	branches, err := r.LocalBranches()
745	if err != nil {
746		return result, err
747	}
748	return MainFirst(sort.StringSlice(branches)), nil
749}
750
751// LocalBranchesWithDeletedTrackingBranches returns the names of all branches
752// whose remote tracking branches have been deleted.
753func (r *Runner) LocalBranchesWithDeletedTrackingBranches() (result []string, err error) {
754	res, err := r.Run("git", "branch", "-vv")
755	if err != nil {
756		return result, err
757	}
758	for _, line := range res.OutputLines() {
759		line = strings.Trim(line, "* ")
760		parts := strings.SplitN(line, " ", 2)
761		branchName := parts[0]
762		deleteTrackingBranchStatus := fmt.Sprintf("[%s: gone]", r.TrackingBranchName(branchName))
763		if strings.Contains(parts[1], deleteTrackingBranchStatus) {
764			result = append(result, branchName)
765		}
766	}
767	return result, nil
768}
769
770// LocalBranchesWithoutMain returns the names of all branches in the local repository,
771// ordered alphabetically without the main branch.
772func (r *Runner) LocalBranchesWithoutMain() (result []string, err error) {
773	mainBranch := r.Config.GetMainBranch()
774	branches, err := r.LocalBranches()
775	if err != nil {
776		return result, err
777	}
778	for _, branch := range branches {
779		if branch != mainBranch {
780			result = append(result, branch)
781		}
782	}
783	return
784}
785
786// MergeBranchNoEdit merges the given branch into the current branch,
787// using the default commit message.
788func (r *Runner) MergeBranchNoEdit(branch string) error {
789	_, err := r.Run("git", "merge", "--no-edit", branch)
790	return err
791}
792
793// PopStash restores stashed-away changes into the workspace.
794func (r *Runner) PopStash() error {
795	_, err := r.Run("git", "stash", "pop")
796	if err != nil {
797		return fmt.Errorf("cannot pop the stash: %w", err)
798	}
799	return nil
800}
801
802// PreviouslyCheckedOutBranch provides the name of the branch that was previously checked out in this repo.
803func (r *Runner) PreviouslyCheckedOutBranch() (name string, err error) {
804	outcome, err := r.Run("git", "rev-parse", "--verify", "--abbrev-ref", "@{-1}")
805	if err != nil {
806		return "", fmt.Errorf("cannot determine the previously checked out branch: %w", err)
807	}
808	return outcome.OutputSanitized(), nil
809}
810
811// Pull fetches updates from the origin remote and updates the currently checked out branch.
812func (r *Runner) Pull() error {
813	_, err := r.Run("git", "pull")
814	if err != nil {
815		return fmt.Errorf("cannot pull updates: %w", err)
816	}
817	return nil
818}
819
820// PushBranch pushes the branch with the given name to the remote.
821func (r *Runner) PushBranch() error {
822	_, err := r.Run("git", "push")
823	if err != nil {
824		return fmt.Errorf("cannot push branch in repo %q to origin: %w", r.WorkingDir(), err)
825	}
826	return nil
827}
828
829// PushBranchForce pushes the branch with the given name to the remote.
830func (r *Runner) PushBranchForce(name string) error {
831	_, err := r.Run("git", "push", "-f", "origin", name)
832	if err != nil {
833		return fmt.Errorf("cannot force-push branch %q in repo %q to origin: %w", name, r.WorkingDir(), err)
834	}
835	return nil
836}
837
838// PushBranchSetUpstream pushes the branch with the given name to the remote.
839func (r *Runner) PushBranchSetUpstream(name string) error {
840	_, err := r.Run("git", "push", "-u", "origin", name)
841	if err != nil {
842		return fmt.Errorf("cannot push branch %q in repo %q to origin: %w", name, r.WorkingDir(), err)
843	}
844	return nil
845}
846
847// PushTags pushes new the Git tags to origin.
848func (r *Runner) PushTags() error {
849	_, err := r.Run("git", "push", "--tags")
850	if err != nil {
851		return fmt.Errorf("cannot push branch in repo %q: %w", r.WorkingDir(), err)
852	}
853	return nil
854}
855
856// Rebase initiates a Git rebase of the current branch against the given branch.
857func (r *Runner) Rebase(target string) error {
858	_, err := r.Run("git", "rebase", target)
859	if err != nil {
860		return fmt.Errorf("cannot rebase against branch %q: %w", target, err)
861	}
862	return nil
863}
864
865// RemoteBranches provides the names of the remote branches in this repo.
866func (r *Runner) RemoteBranches() ([]string, error) {
867	if !r.RemoteBranchCache.Initialized() {
868		outcome, err := r.Run("git", "branch", "-r")
869		if err != nil {
870			return []string{}, fmt.Errorf("cannot determine remote branches: %w", err)
871		}
872		lines := outcome.OutputLines()
873		branches := make([]string, 0, len(lines)-1)
874		for l := range lines {
875			if !strings.Contains(lines[l], " -> ") {
876				branches = append(branches, strings.TrimSpace(lines[l]))
877			}
878		}
879		r.RemoteBranchCache.Set(branches)
880	}
881	return r.RemoteBranchCache.Value(), nil
882}
883
884// Remotes provides the names of all Git remotes in this repository.
885func (r *Runner) Remotes() (result []string, err error) {
886	if !r.RemotesCache.initialized {
887		out, err := r.Run("git", "remote")
888		if err != nil {
889			return result, fmt.Errorf("cannot determine remotes: %w", err)
890		}
891		if out.OutputSanitized() == "" {
892			r.RemotesCache.Set([]string{})
893		} else {
894			r.RemotesCache.Set(out.OutputLines())
895		}
896	}
897	return r.RemotesCache.Value(), nil
898}
899
900// RemoveBranch deletes the branch with the given name from this repo.
901func (r *Runner) RemoveBranch(name string) error {
902	_, err := r.Run("git", "branch", "-D", name)
903	if err != nil {
904		return fmt.Errorf("cannot delete branch %q: %w", name, err)
905	}
906	return nil
907}
908
909// RemoveRemote deletes the Git remote with the given name.
910func (r *Runner) RemoveRemote(name string) error {
911	r.RemotesCache.Invalidate()
912	_, err := r.Run("git", "remote", "rm", name)
913	return err
914}
915
916// RemoveUnnecessaryFiles trims all files that aren't necessary in this repo.
917func (r *Runner) RemoveUnnecessaryFiles() error {
918	fullPath := filepath.Join(r.WorkingDir(), ".git", "hooks")
919	err := os.RemoveAll(fullPath)
920	if err != nil {
921		return fmt.Errorf("cannot remove unnecessary files in %q: %w", fullPath, err)
922	}
923	_ = os.Remove(filepath.Join(r.WorkingDir(), ".git", "COMMIT_EDITMSG"))
924	_ = os.Remove(filepath.Join(r.WorkingDir(), ".git", "description"))
925	return nil
926}
927
928// ResetToSha undoes all commits on the current branch all the way until the given SHA.
929func (r *Runner) ResetToSha(sha string, hard bool) error {
930	args := []string{"reset"}
931	if hard {
932		args = append(args, "--hard")
933	}
934	args = append(args, sha)
935	_, err := r.Run("git", args...)
936	if err != nil {
937		return fmt.Errorf("cannot reset to SHA %q: %w", sha, err)
938	}
939	return nil
940}
941
942// RevertCommit reverts the commit with the given SHA.
943func (r *Runner) RevertCommit(sha string) error {
944	_, err := r.Run("git", "revert", sha)
945	if err != nil {
946		return fmt.Errorf("cannot revert commit %q: %w", sha, err)
947	}
948	return nil
949}
950
951// RootDirectory returns the path of the rood directory of the current repository,
952// i.e. the directory that contains the ".git" folder.
953func (r *Runner) RootDirectory() (string, error) {
954	if !r.RootDirCache.Initialized() {
955		res, err := r.Run("git", "rev-parse", "--show-toplevel")
956		if err != nil {
957			return "", fmt.Errorf("cannot determine root directory: %w", err)
958		}
959		r.RootDirCache.Set(filepath.FromSlash(res.OutputSanitized()))
960	}
961	return r.RootDirCache.Value(), nil
962}
963
964// ShaForBranch provides the SHA for the local branch with the given name.
965func (r *Runner) ShaForBranch(name string) (sha string, err error) {
966	outcome, err := r.Run("git", "rev-parse", name)
967	if err != nil {
968		return "", fmt.Errorf("cannot determine SHA of local branch %q: %w", name, err)
969	}
970	return outcome.OutputSanitized(), nil
971}
972
973// ShaForCommit provides the SHA for the commit with the given name.
974func (r *Runner) ShaForCommit(name string) (result string, err error) {
975	var args []string
976	if name == "Initial commit" {
977		args = []string{"reflog", "--grep=" + name, "--format=%H", "--max-count=1"}
978	} else {
979		args = []string{"reflog", "--grep-reflog=commit: " + name, "--format=%H"}
980	}
981	res, err := r.Run("git", args...)
982	if err != nil {
983		return result, fmt.Errorf("cannot determine SHA of commit %q: %w", name, err)
984	}
985	if res.OutputSanitized() == "" {
986		return result, fmt.Errorf("cannot find the SHA of commit %q", name)
987	}
988	return res.OutputSanitized(), nil
989}
990
991// ShouldPushBranch returns whether the local branch with the given name
992// contains commits that have not been pushed to the remote.
993func (r *Runner) ShouldPushBranch(branch string) (bool, error) {
994	trackingBranch := r.TrackingBranchName(branch)
995	out, err := r.Run("git", "rev-list", "--left-right", branch+"..."+trackingBranch)
996	if err != nil {
997		return false, fmt.Errorf("cannot list diff of %q and %q: %w", branch, trackingBranch, err)
998	}
999	return out.OutputSanitized() != "", nil
1000}
1001
1002// SquashMerge squash-merges the given branch into the current branch.
1003func (r *Runner) SquashMerge(branch string) error {
1004	_, err := r.Run("git", "merge", "--squash", branch)
1005	if err != nil {
1006		return fmt.Errorf("cannot squash-merge branch %q: %w", branch, err)
1007	}
1008	return nil
1009}
1010
1011// Stash adds the current files to the Git stash.
1012func (r *Runner) Stash() error {
1013	err := r.RunMany([][]string{
1014		{"git", "add", "-A"},
1015		{"git", "stash"},
1016	})
1017	if err != nil {
1018		return fmt.Errorf("cannot stash: %w", err)
1019	}
1020	return nil
1021}
1022
1023// StashSize provides the number of stashes in this repository.
1024func (r *Runner) StashSize() (result int, err error) {
1025	res, err := r.Run("git", "stash", "list")
1026	if err != nil {
1027		return result, fmt.Errorf("command %q failed: %w", res.FullCmd(), err)
1028	}
1029	if res.OutputSanitized() == "" {
1030		return 0, nil
1031	}
1032	return len(res.OutputLines()), nil
1033}
1034
1035// Tags provides a list of the tags in this repository.
1036func (r *Runner) Tags() (result []string, err error) {
1037	res, err := r.Run("git", "tag")
1038	if err != nil {
1039		return result, fmt.Errorf("cannot determine tags in repo %q: %w", r.WorkingDir(), err)
1040	}
1041	for _, line := range strings.Split(res.OutputSanitized(), "\n") {
1042		result = append(result, strings.TrimSpace(line))
1043	}
1044	return result, err
1045}
1046
1047// TrackingBranchName provides the name of the remote branch tracking the given local branch.
1048func (r *Runner) TrackingBranchName(branch string) string {
1049	return "origin/" + branch
1050}
1051
1052// UncommittedFiles provides the names of the files not committed into Git.
1053func (r *Runner) UncommittedFiles() (result []string, err error) {
1054	res, err := r.Run("git", "status", "--porcelain", "--untracked-files=all")
1055	if err != nil {
1056		return result, fmt.Errorf("cannot determine uncommitted files in %q: %w", r.WorkingDir(), err)
1057	}
1058	lines := res.OutputLines()
1059	for l := range lines {
1060		if lines[l] == "" {
1061			continue
1062		}
1063		parts := strings.Split(lines[l], " ")
1064		result = append(result, parts[1])
1065	}
1066	return result, nil
1067}
1068
1069// StageFiles adds the file with the given name to the Git index.
1070func (r *Runner) StageFiles(names ...string) error {
1071	args := append([]string{"add"}, names...)
1072	_, err := r.Run("git", args...)
1073	if err != nil {
1074		return fmt.Errorf("cannot stage files %s: %w", strings.Join(names, ", "), err)
1075	}
1076	return nil
1077}
1078
1079// StartCommit starts a commit and stops at asking the user for the commit message.
1080func (r *Runner) StartCommit() error {
1081	_, err := r.Run("git", "commit")
1082	if err != nil {
1083		return fmt.Errorf("cannot start commit: %w", err)
1084	}
1085	return nil
1086}
1087
1088// Version indicates whether the needed Git version is installed.
1089func (r *Runner) Version() (major int, minor int, err error) {
1090	versionRegexp := regexp.MustCompile(`git version (\d+).(\d+).(\d+)`)
1091	res, err := r.Run("git", "version")
1092	if err != nil {
1093		return 0, 0, fmt.Errorf("cannot determine Git version: %w", err)
1094	}
1095	matches := versionRegexp.FindStringSubmatch(res.OutputSanitized())
1096	if matches == nil {
1097		return 0, 0, fmt.Errorf("'git version' returned unexpected output: %q.\nPlease open an issue and supply the output of running 'git version'", res.Output())
1098	}
1099	majorVersion, err := strconv.Atoi(matches[1])
1100	if err != nil {
1101		return 0, 0, fmt.Errorf("cannot convert major version %q to int: %w", matches[1], err)
1102	}
1103	minorVersion, err := strconv.Atoi(matches[2])
1104	if err != nil {
1105		return 0, 0, fmt.Errorf("cannot convert minor version %q to int: %w", matches[2], err)
1106	}
1107	return majorVersion, minorVersion, nil
1108}
1109