summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorMark Kopenga <mkopenga@gmail.com>2018-08-14 11:05:26 +0200
committerMark Kopenga <mkopenga@gmail.com>2018-08-14 11:05:26 +0200
commitdfafb988713a79664139d15ad471736a5a4b1b90 (patch)
treeebde4bd5c64ce8e1fe5e8670657c708984b0e8ff /pkg
parentf2dfcb6e12d78f3e7b890d5bd43be7b032e1df88 (diff)
tried to update to latest master
Diffstat (limited to 'pkg')
-rw-r--r--pkg/app/app.go71
-rw-r--r--pkg/commands/branch.go39
-rw-r--r--pkg/commands/git.go497
-rw-r--r--pkg/commands/git_structs.go36
-rw-r--r--pkg/commands/os.go174
-rw-r--r--pkg/config/app_config.go45
-rw-r--r--pkg/git/branch_list_builder.go160
-rw-r--r--pkg/gui/branches_panel.go141
-rw-r--r--pkg/gui/commit_message_panel.go50
-rw-r--r--pkg/gui/commits_panel.go176
-rw-r--r--pkg/gui/confirmation_panel.go149
-rw-r--r--pkg/gui/files_panel.go400
-rw-r--r--pkg/gui/gui.go331
-rw-r--r--pkg/gui/keybindings.go93
-rw-r--r--pkg/gui/merge_panel.go260
-rw-r--r--pkg/gui/stash_panel.go97
-rw-r--r--pkg/gui/status_panel.go42
-rw-r--r--pkg/gui/view_helpers.go245
-rw-r--r--pkg/i18n/i18n.go108
-rw-r--r--pkg/utils/utils.go65
20 files changed, 3179 insertions, 0 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go
new file mode 100644
index 000000000..d558ed250
--- /dev/null
+++ b/pkg/app/app.go
@@ -0,0 +1,71 @@
+package app
+
+import (
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/jesseduffield/lazygit/pkg/commands"
+ "github.com/jesseduffield/lazygit/pkg/config"
+ "github.com/jesseduffield/lazygit/pkg/gui"
+)
+
+// App struct
+type App struct {
+ closers []io.Closer
+
+ Config config.AppConfigurer
+ Log *logrus.Logger
+ OSCommand *commands.OSCommand
+ GitCommand *commands.GitCommand
+ Gui *gui.Gui
+}
+
+func newLogger(config config.AppConfigurer) *logrus.Logger {
+ log := logrus.New()
+ if !config.GetDebug() {
+ log.Out = ioutil.Discard
+ return log
+ }
+ file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+ if err != nil {
+ panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
+ }
+ log.SetOutput(file)
+ return log
+}
+
+// NewApp retruns a new applications
+func NewApp(config config.AppConfigurer) (*App, error) {
+ app := &App{
+ closers: []io.Closer{},
+ Config: config,
+ }
+ var err error
+ app.Log = newLogger(config)
+ app.OSCommand, err = commands.NewOSCommand(app.Log)
+ if err != nil {
+ return nil, err
+ }
+ app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand)
+ if err != nil {
+ return nil, err
+ }
+ app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, config.GetVersion())
+ if err != nil {
+ return nil, err
+ }
+ return app, nil
+}
+
+// Close closes any resources
+func (app *App) Close() error {
+ for _, closer := range app.closers {
+ err := closer.Close()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/pkg/commands/branch.go b/pkg/commands/branch.go
new file mode 100644
index 000000000..13c26e766
--- /dev/null
+++ b/pkg/commands/branch.go
@@ -0,0 +1,39 @@
+package commands
+
+import (
+ "strings"
+
+ "github.com/fatih/color"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+// Branch : A git branch
+// duplicating this for now
+type Branch struct {
+ Name string
+ Recency string
+}
+
+// GetDisplayString returns the dispaly string of branch
+func (b *Branch) GetDisplayString() string {
+ return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
+}
+
+// GetColor branch color
+func (b *Branch) GetColor() color.Attribute {
+ switch b.getType() {
+ case "feature":
+ return color.FgGreen
+ case "bugfix":
+ return color.FgYellow
+ case "hotfix":
+ return color.FgRed
+ default:
+ return color.FgWhite
+ }
+}
+
+// expected to return feature/bugfix/hotfix or blank string
+func (b *Branch) getType() string {
+ return strings.Split(b.Name, "/")[0]
+}
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
new file mode 100644
index 000000000..44fd57f1c
--- /dev/null
+++ b/pkg/commands/git.go
@@ -0,0 +1,497 @@
+package commands
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+ gitconfig "github.com/tcnksm/go-gitconfig"
+ gogit "gopkg.in/src-d/go-git.v4"
+)
+
+// GitCommand is our main git interface
+type GitCommand struct {
+ Log *logrus.Logger
+ OSCommand *OSCommand
+ Worktree *gogit.Worktree
+ Repo *gogit.Repository
+}
+
+// NewGitCommand it runs git commands
+func NewGitCommand(log *logrus.Logger, osCommand *OSCommand) (*GitCommand, error) {
+ gitCommand := &GitCommand{
+ Log: log,
+ OSCommand: osCommand,
+ }
+ return gitCommand, nil
+}
+
+// SetupGit sets git repo up
+func (c *GitCommand) SetupGit() {
+ c.verifyInGitRepo()
+ c.navigateToRepoRootDirectory()
+ c.setupWorktree()
+}
+
+// GetStashEntries stash entryies
+func (c *GitCommand) GetStashEntries() []StashEntry {
+ stashEntries := make([]StashEntry, 0)
+ rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
+ for i, line := range utils.SplitLines(rawString) {
+ stashEntries = append(stashEntries, stashEntryFromLine(line, i))
+ }
+ return stashEntries
+}
+
+func stashEntryFromLine(line string, index int) StashEntry {
+ return StashEntry{
+ Name: line,
+ Index: index,
+ DisplayString: line,
+ }
+}
+
+// GetStashEntryDiff stash diff
+func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
+ return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
+}
+
+func includes(array []string, str string) bool {
+ for _, arrayStr := range array {
+ if arrayStr == str {
+ return true
+ }
+ }
+ return false
+}
+
+// GetStatusFiles git status files
+func (c *GitCommand) GetStatusFiles() []File {
+ statusOutput, _ := c.GitStatus()
+ statusStrings := utils.SplitLines(statusOutput)
+ files := make([]File, 0)
+
+ for _, statusString := range statusStrings {
+ change := statusString[0:2]
+ stagedChange := change[0:1]
+ unstagedChange := statusString[1:2]
+ filename := statusString[3:]
+ tracked := !includes([]string{"??", "A "}, change)
+ file := File{
+ Name: filename,
+ DisplayString: statusString,
+ HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
+ HasUnstagedChanges: unstagedChange != " ",
+ Tracked: tracked,
+ Deleted: unstagedChange == "D" || stagedChange == "D",
+ HasMergeConflicts: change == "UU",
+ }
+ files = append(files, file)
+ }
+ c.Log.Info(files) // TODO: use a dumper-esque log here
+ return files
+}
+
+// StashDo modify stash
+func (c *GitCommand) StashDo(index int, method string) error {
+ return c.OSCommand.RunCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
+}
+
+// StashSave save stash
+// TODO: before calling this, check if there is anything to save
+func (c *GitCommand) StashSave(message string) error {
+ return c.OSCommand.RunCommand("git stash save " + c.OSCommand.Quote(message))
+}
+
+// MergeStatusFiles merge status files
+func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
+ if len(oldFiles) == 0 {
+ return newFiles
+ }
+
+ appendedIndexes := make([]int, 0)
+
+ // retain position of files we already could see
+ result := make([]File, 0)
+ for _, oldFile := range oldFiles {
+ for newIndex, newFile := range newFiles {
+ if oldFile.Name == newFile.Name {
+ result = append(result, newFile)
+ appendedIndexes = append(appendedIndexes, newIndex)
+ break
+ }
+ }
+ }
+
+ // append any new files to the end
+ for index, newFile := range newFiles {
+ if !includesInt(appendedIndexes, index) {
+ result = append(result, newFile)
+ }
+ }
+
+ return result
+}
+
+func (c *GitCommand) verifyInGitRepo() {
+ if output, err := c.OSCommand.RunCommandWithOutput("git status"); err != nil {
+ fmt.Println(output)
+ os.Exit(1)
+ }
+}
+
+// GetBranchName branch name
+func (c *GitCommand) GetBranchName() (string, error) {
+ return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
+}
+
+func (c *GitCommand) navigateToRepoRootDirectory() {
+ _, err := os.Stat(".git")
+ for os.IsNotExist(err) {
+ c.Log.Debug("going up a directory to find the root")
+ os.Chdir("..")
+ _, err = os.Stat(".git")
+ }
+}
+
+func (c *GitCommand) setupWorktree() {
+ r, err := gogit.PlainOpen(".")
+ if err != nil {
+ panic(err)
+ }
+ c.Repo = r
+
+ w, err := r.Worktree()
+ if err != nil {
+ panic(err)
+ }
+ c.Worktree = w
+}
+
+// ResetHard does the equivalent of `git reset --hard HEAD`
+func (c *GitCommand) ResetHard() error {
+ return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
+}
+
+// UpstreamDifferenceCount checks how many pushables/pullables there are for the
+// current branch
+func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
+ pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --count")
+ if err != nil {
+ return "?", "?"
+ }
+ pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list head..@{u} --count")
+ if err != nil {
+ return "?", "?"
+ }
+ return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
+}
+
+// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
+// to the remote branch of the current branch
+func (c *GitCommand) GetCommitsToPush() []string {
+ pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
+ if err != nil {
+ return make([]string, 0)
+ }
+ return utils.SplitLines(pushables)
+}
+
+// RenameCommit renames the topmost commit with the given name
+func (c *GitCommand) RenameCommit(name string) error {
+ return c.OSCommand.RunCommand("git commit --allow-empty --amend -m " + c.OSCommand.Quote(name))
+}
+
+// Fetch fetch git repo
+func (c *GitCommand) Fetch() error {
+ return c.OSCommand.RunCommand("git fetch")
+}
+
+// ResetToCommit reset to commit
+func (c *GitCommand) ResetToCommit(sha string) error {
+ return c.OSCommand.RunCommand("git reset " + sha)
+}
+
+// NewBranch create new branch
+func (c *GitCommand) NewBranch(name string) error {
+ return c.OSCommand.RunCommand("git checkout -b " + name)
+}
+
+// DeleteBranch delete branch
+func (c *GitCommand) DeleteBranch(branch string) error {
+ return c.OSCommand.RunCommand("git branch -d " + branch)
+}
+
+// ListStash list stash
+func (c *GitCommand) ListStash() (string, error) {
+ return c.OSCommand.RunCommandWithOutput("git stash list")
+}
+
+// Merge merge
+func (c *GitCommand) Merge(branchName string) error {
+ return c.OSCommand.RunCommand("git merge --no-edit " + branchName)
+}
+
+// AbortMerge abort merge
+func (c *GitCommand) AbortMerge() error {
+ return c.OSCommand.RunCommand("git merge --abort")
+}
+
+// UsingGpg tells us whether the user has gpg enabled so that we can know
+// whether we need to run a subprocess to allow them to enter their password
+func (c *GitCommand) UsingGpg() bool {
+ gpgsign, _ := gitconfig.Global("commit.gpgsign")
+ if gpgsign == "" {
+ gpgsign, _ = gitconfig.Local("commit.gpgsign")
+ }
+ if gpgsign == "" {
+ return false
+ }
+ return true
+}
+
+// Commit commit to git
+func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) {
+ command := "git commit -m " + c.OSCommand.Quote(message)
+ if c.UsingGpg() {
+ return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command)
+ }
+ return nil, c.OSCommand.RunCommand(command)
+}
+
+// Pull pull from repo
+func (c *GitCommand) Pull() error {
+ return c.OSCommand.RunCommand("git pull --no-edit")
+}
+
+// Push push to a branch
+func (c *GitCommand) Push(branchName string) error {
+ return c.OSCommand.RunCommand("git push -u origin " + branchName)
+}
+
+// SquashPreviousTwoCommits squashes a commit down to the one below it
+// retaining the message of the higher commit
+func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
+ // TODO: test this
+ err := c.OSCommand.RunCommand("git reset --soft HEAD^")
+ if err != nil {
+ return err
+ }
+ // TODO: if password is required, we need to return a subprocess
+ return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message))
+}
+
+// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
+// retaining the commit message of the lower commit
+func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
+ var err error
+ commands := []string{
+ "git checkout -q " + shaValue,
+ "git reset --soft " + shaValue + "^",
+ "git commit --amend -C " + shaValue + "^",
+ "git rebase --onto HEAD " + shaValue + " " + branchName,
+ }
+ ret := ""
+ for _, command := range commands {
+ c.Log.Info(command)
+ output, err := c.OSCommand.RunCommandWithOutput(command)
+ ret += output
+ if err != nil {
+ c.Log.Info(ret)
+ break
+ }
+ }
+ if err != nil {
+ // We are already in an error state here so we're just going to append
+ // the output of these commands
+ output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue)
+ ret += output
+ output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName)
+ ret += output
+ }
+ if err != nil {
+ return errors.New(ret)
+ }
+ return nil
+}
+
+// CatFile obtain the contents of a file
+func (c *GitCommand) CatFile(file string) (string, error) {
+ return c.OSCommand.RunCommandWithOutput("cat " + file)
+}
+
+// StageFile stages a file
+func (c *GitCommand) StageFile(file string) error {
+ return c.OSCommand.RunCommand("git add " + c.OSCommand.Quote(file))
+}
+
+// UnStageFile unstages a file
+func (c *GitCommand) UnStageFile(file string, tracked bool) error {
+ var command string
+ if tracked {
+ command = "git reset HEAD "
+ } else {
+ command = "git rm --cached "
+ }
+ return c.OSCommand.RunCommand(command + file)
+}
+
+// GitStatus returns the plaintext short status of the repo
+func (c *GitCommand) GitStatus() (string, error) {
+ return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
+}
+
+// IsInMergeState states whether we are still mid-merge
+func (c *GitCommand) IsInMergeState() (bool, error) {
+ output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
+ if err != nil {
+ return false, err
+ }
+ return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
+}
+
+// RemoveFile directly
+func (c *GitCommand) RemoveFile(file File) error {
+ // if the file isn't tracked, we assume you want to delete it
+ if !file.Tracked {
+ return c.OSCommand.RunCommand("rm -rf ./" + file.Name)
+ }
+ // if the file is tracked, we assume you want to just check it out
+ return c.OSCommand.RunCommand("git checkout " + file.Name)
+}
+
+// Checkout checks out a branch, with --force if you set the force arg to true
+func (c *GitCommand) Checkout(branch string, force bool) error {
+ forceArg := ""
+ if force {
+ forceArg = "--force "
+ }
+ return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
+}
+
+// AddPatch prepares a subprocess for adding a patch by patch
+// this will eventually be swapped out for a better solution inside the Gui
+func (c *GitCommand) AddPatch(filename string) (*exec.Cmd, error) {
+ return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
+}
+
+// PrepareCommitSubProcess prepares a subprocess for `git commit`
+func (c *GitCommand) PrepareCommitSubProcess() (*exec.Cmd, error) {
+ return c.OSCommand.PrepareSubProcess("git", "commit")
+}
+
+// GetBranchGraph gets the color-formatted graph of the log for the given branch
+// Currently it limits the result to 100 commits, but when we get async stuff
+// working we can do lazy loading
+func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
+ return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName)
+}
+
+// Map (from https://gobyexample.com/collection-functions)
+func Map(vs []string, f func(string) string) []string {
+ vsm := make([]string, len(vs))
+ for i, v := range vs {
+ vsm[i] = f(v)
+ }
+ return vsm
+}
+
+func includesString(list []string, a string) bool {
+ for _, b := range list {
+ if b == a {
+ return true
+ }
+ }
+ return false
+}
+
+// not sure how to genericise this because []interface{} doesn't accept e.g.
+// []int arguments
+func includesInt(list []int, a int) bool {
+ for _, b := range list {
+ if b == a {
+ return true
+ }
+ }
+ return false
+}
+
+// GetCommits obtains the commits of the current branch
+func (c *GitCommand) GetCommits() []Commit {
+ pushables := c.GetCommitsToPush()
+ log := c.GetLog()
+ commits := make([]Commit, 0)
+ // now we can split it up and turn it into commits
+ lines := utils.SplitLines(log)
+ for _, line := range lines {
+ splitLine := strings.Split(line, " ")
+ sha := splitLine[0]
+ pushed := includesString(pushables, sha)
+ commits = append(commits, Commit{
+ Sha: sha,
+ Name: strings.Join(splitLine[1:], " "),
+ Pushed: pushed,
+ DisplayString: strings.Join(splitLine, " "),
+ })
+ }
+ return commits
+}
+
+// GetLog gets the git log (currently limited to 30 commits for performance
+// until we work out lazy loading
+func (c *GitCommand) GetLog() string {
+ // currently limiting to 30 for performance reasons
+ // TODO: add lazyloading when you scroll down
+ result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
+ if err != nil {
+ // assume if there is an error there are no commits yet for this branch
+ return ""
+ }
+ return result
+}
+
+// Ignore adds a file to the gitignore for the repo
+func (c *GitCommand) Ignore(filename string) {
+ if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
+ panic(err)
+ }
+}
+
+// Show shows the diff of a commit
+func (c *GitCommand) Show(sha string) string {
+ result, err := c.OSCommand.RunCommandWithOutput("git show --color " + sha)
+ if err != nil {
+ panic(err)
+ }
+ return result
+}
+
+// Diff returns the diff of a file
+func (c *GitCommand) Diff(file File) string {
+ cachedArg := ""
+ fileName := file.Name
+ if file.HasStagedChanges && !file.HasUnstagedChanges {
+ cachedArg = "--cached"
+ } else {
+ // if the file is staged and has spaces in it, it comes pre-quoted
+ fileName = c.OSCommand.Quote(fileName)
+ }
+ deletedArg := ""
+ if file.Deleted {
+ deletedArg = "--"
+ }
+ trackedArg := ""
+ if !file.Tracked && !file.HasStagedChanges {
+ trackedArg = "--no-index /dev/null"
+ }
+ command := fmt.Sprintf("%s %s %s %s %s", "git diff --color ", cachedArg, deletedArg, trackedArg, fileName)
+
+ // for now we assume an error means the file was deleted
+ s, _ := c.OSCommand.RunCommandWithOutput(command)
+ return s
+}
diff --git a/pkg/commands/git_structs.go b/pkg/commands/git_structs.go
new file mode 100644
index 000000000..6b10b18bb
--- /dev/null
+++ b/pkg/commands/git_structs.go
@@ -0,0 +1,36 @@
+package commands
+
+// File : A staged/unstaged file
+// TODO: decide whether to give all of these the Git prefix
+type File struct {
+ Name string
+ HasStagedChanges bool
+ HasUnstagedChanges bool
+ Tracked bool
+ Deleted bool
+ HasMergeConflicts bool
+ DisplayString string
+}
+
+// Commit : A git commit
+type Commit struct {
+ Sha string
+ Name string
+ Pushed bool
+ DisplayString string
+}
+
+// StashEntry : A git stash entry
+type StashEntry struct {
+ Index int
+ Name string
+ DisplayString string
+}
+
+// Conflict : A git conflict with a start middle and end corresponding to line
+// numbers in the file where the conflict bars appear
+type Conflict struct {
+ Start int
+ Middle int
+ End int
+}
diff --git a/pkg/commands/os.go b/pkg/commands/os.go
new file mode 100644
index 000000000..9f9819a5a
--- /dev/null
+++ b/pkg/commands/os.go
@@ -0,0 +1,174 @@
+package commands
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "runtime"
+
+ "github.com/davecgh/go-spew/spew"
+
+ "github.com/mgutz/str"
+
+ "github.com/Sirupsen/logrus"
+ gitconfig "github.com/tcnksm/go-gitconfig"
+)
+
+var (
+ // ErrNoOpenCommand : When we don't know which command to use to open a file
+ ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
+ // ErrNoEditorDefined : When we can't find an editor to edit a file
+ ErrNoEditorDefined = errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
+)
+
+// Platform stores the os state
+type Platform struct {
+ os string
+ shell string
+ shellArg string
+ escapedQuote string
+}
+
+// OSCommand holds all the os commands
+type OSCommand struct {
+ Log *logrus.Logger
+ Platform *Platform
+}
+
+// NewOSCommand os command runner
+func NewOSCommand(log *logrus.Logger) (*OSCommand, error) {
+ osCommand := &OSCommand{
+ Log: log,
+ Platform: getPlatform(),
+ }
+ return osCommand, nil
+}
+
+// RunCommandWithOutput wrapper around commands returning their output and error
+func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
+ c.Log.WithField("command", command).Info("RunCommand")
+ splitCmd := str.ToArgv(command)
+ c.Log.Info(splitCmd)
+ cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
+ return sanitisedCommandOutput(cmdOut, err)
+}
+
+// RunCommand runs a command and just returns the error
+func (c *OSCommand) RunCommand(command string) error {
+ _, err := c.RunCommandWithOutput(command)
+ return err
+}
+
+// RunDirectCommand wrapper around direct commands
+func (c *OSCommand) RunDirectCommand(command string) (string, error) {
+ c.Log.WithField("command", command).Info("RunDirectCommand")
+ args := str.ToArgv(c.Platform.shellArg + " " + command)
+ c.Log.Info(spew.Sdump(args))
+
+ cmdOut, err := exec.
+ Command(c.Platform.shell, args...).
+ CombinedOutput()
+ return sanitisedCommandOutput(cmdOut, err)
+}
+
+func sanitisedCommandOutput(output []byte, err error) (string, error) {
+ outputString := string(output)
+ if err != nil {
+ // errors like 'exit status 1' are not very useful so we'll create an error
+ // from the combined output
+ return outputString, errors.New(outputString)
+ }
+ return outputString, nil
+}
+
+func getPlatform() *Platform {
+ switch runtime.GOOS {
+ case "windows":
+ return &Platform{
+ os: "windows",
+ shell: "cmd",
+ shellArg: "/c",
+ escapedQuote: "\\\"",
+ }
+ default:
+ return &Platform{
+ os: runtime.GOOS,
+ shell: "bash",
+ shellArg: "-c",
+ escapedQuote: "\"",
+ }
+ }
+}
+
+// GetOpenCommand get open command
+func (c *OSCommand) GetOpenCommand() (string, string, error) {
+ //NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
+ trailMap := map[string]string{
+ "xdg-open": " &>/dev/null &",
+ "cygstart": "",
+ "open": "",
+ }
+ for name, trail := range trailMap {
+ if err := c.RunCommand("which " + name); err == nil {
+ return name, trail, nil
+ }
+ }
+ return "", "", ErrNoOpenCommand
+}
+
+// VsCodeOpenFile opens the file in code, with the -r flag to open in the
+// current window
+// each of these open files needs to have the same function signature because
+// they're being passed as arguments into another function,
+// but only editFile actually returns a *exec.Cmd
+func (c *OSCommand) VsCodeOpenFile(filename string) (*exec.Cmd, error) {
+ return nil, c.RunCommand("code -r " + filename)
+}
+
+// SublimeOpenFile opens the filein sublime
+// may be deprecated in the future
+func (c *OSCommand) SublimeOpenFile(filename string) (*exec.Cmd, error) {
+ return nil, c.RunCommand("subl " + filename)
+}
+
+// OpenFile opens a file with the given
+func (c *OSCommand) OpenFile(filename string) (*exec.Cmd, error) {
+ cmdName, cmdTrail, err := c.GetOpenCommand()
+ if err != nil {
+ return nil, err
+ }
+ err = c.RunCommand(cmdName + " " + filename + cmdTrail) // TODO: test on linux
+ return nil, err
+}
+
+// EditFile opens a file in a subprocess using whatever editor is available,
+// falling back to core.editor, VISUAL, EDITOR, then vi
+func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
+ editor, _ := gitconfig.Global("core.editor")
+ if editor == "" {
+ editor = os.Getenv("VISUAL")
+ }
+ if editor == "" {
+ editor = os.Getenv("EDITOR")
+ }
+ if editor == "" {
+ if err := c.RunCommand("which vi"); err == nil {
+ editor = "vi"
+ }
+ }
+ if editor == "" {
+ return nil, ErrNoEditorDefined
+ }
+ return c.PrepareSubProcess(editor, filename)
+}
+
+// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
+func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) {
+ subprocess := exec.Command(cmdName, commandArgs...)
+ return subprocess, nil
+}
+
+// Quote wraps a message in platform-specific quotation marks
+func (c *OSCommand) Quote(message string) string {
+ return c.Platform.escapedQuote + message + c.Platform.escapedQuote
+}
diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go
new file mode 100644
index 000000000..98e56dea2
--- /dev/null
+++ b/pkg/config/app_config.go
@@ -0,0 +1,45 @@
+package config
+
+// AppConfig contains the base configuration fields required for lazygit.
+type AppConfig struct {
+ Debug bool `long:"debug" env:"DEBUG" default:"false"`
+ Version string `long:"version" env:"VERSION" default:"unversioned"`
+ Commit string `long:"commit" env:"COMMIT"`
+ BuildDate string `long:"build-date" env:"BUILD_DATE"`
+ Name string `long:"name" env:"NAME" default:"lazygit"`
+}
+
+// AppConfigurer interface allows individual app config structs to inherit Fields
+// from AppConfig and still be used by lazygit.
+type AppConfigurer interface {
+ GetDebug() bool
+ GetVersion() string
+ GetCommit() string
+ GetBuildDate() string
+ GetName() string
+}
+
+// GetDebug returns debug flag
+func (c *AppConfig) GetDebug() bool {
+ return c.Debug
+}
+
+// GetVersion returns debug flag
+func (c *AppConfig) GetVersion() string {
+ return c.Version
+}
+
+// GetCommit returns debug flag
+func (c *AppConfig) GetCommit() string {
+ return c.Commit
+}
+
+// GetBuildDate returns debug flag
+func (c *AppConfig) GetBuildDate() string {
+ return c.BuildDate
+}
+
+// GetName returns debug flag
+func (c *AppConfig) GetName() string {
+ return c.Name
+}
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
new file mode 100644
index 000000000..41e59c093
--- /dev/null
+++ b/pkg/git/branch_list_builder.go
@@ -0,0 +1,160 @@