diff options
| author | Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> | 2018-12-10 03:00:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-12-10 03:00:24 +0300 |
| commit | 0f55d3652c215074df03de4390856d4389a1b729 (patch) | |
| tree | 852ca843d5a67852d69054f816bb8b746df8b49e | |
| parent | Merge pull request #28 from isacikgoz/develop (diff) | |
| parent | added recursive option and some minor code formating (diff) | |
| download | gitbatch-0f55d3652c215074df03de4390856d4389a1b729.tar.gz | |
Merge pull request #29 from isacikgoz/develop
Develop
32 files changed, 1357 insertions, 218 deletions
@@ -10,7 +10,6 @@ Aim of this tool to make your local repositories synchronized with remotes easil - [BitBucket Set up an SSH key](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-git-728138079.html) - Feedbacks are welcome. For now, known issues are: - At very low probability app fails to load repositories, try again it will load next time (multithreading problem). - - Sometimes when you scroll too fast while pulling/fetching/merging, some multithreading problem occurs and app crashes (will fix soon). - Colors vary to your terminal theme colors, so if the contrast is not enough on some color decisions; discussions are welcome. Here is the screencast of the app: @@ -44,15 +43,16 @@ For more information; - select all feature ✔ - arrange repositories to an order e.g. alphabetic, last modified, etc. ✔ - shift keys, i.e. **s** for iterate **alt + s** for reverse iteration ✔ -- recursive repository search from the filesystem +- recursive repository search from the filesystem ✔ - full src-d/go-git integration (*having some performance issues*) -- implement config file to pre-define repo locations or some settings +- implement config file to pre-define repo locations or some settings ✔ - resolve authentication issues ## Credits -- [go-git](https://github.com/src-d/go-git) for git interface +- [go-git](https://github.com/src-d/go-git) for git interface (partially) - [gocui](https://github.com/jroimartin/gocui) for user interface - [logrus](https://github.com/sirupsen/logrus) for logging -- [lazygit](https://github.com/jesseduffield/lazygit) as app template +- [viper](https://github.com/spf13/viper) for configuration management - [color](https://github.com/fatih/color) for colored text +- [lazygit](https://github.com/jesseduffield/lazygit) as app template and reference - [kingpin](https://github.com/alecthomas/kingpin) for command-line flag&options @@ -11,18 +11,24 @@ import ( var ( // take this as default directory if user does not start app with -d flag currentDir, err = os.Getwd() - dir = kingpin.Flag("directory", "Directory to roam for git repositories.").Default(currentDir).Short('d').String() - repoPattern = kingpin.Flag("pattern", "Pattern to filter repositories").Short('p').String() + dirs = kingpin.Flag("directory", "Directory to roam for git repositories").Default(currentDir).Short('d').Strings() + ignoreConfig = kingpin.Flag("ignore-config", "Ignore config file").Short('i').Bool() + recurseDepth = kingpin.Flag("recursive-depth", "Find directories recursively").Default("1").Short('r').Int() logLevel = kingpin.Flag("log-level", "Logging level; trace,debug,info,warn,error").Default("error").Short('l').String() ) func main() { - kingpin.Version("gitbatch version 0.0.1 (alpha)") + kingpin.Version("gitbatch version 0.0.2 (alpha)") // parse the command line flag and options kingpin.Parse() // set the app - app, err := app.Setup(*dir, *repoPattern, *logLevel) + app, err := app.Setup(app.SetupConfig{ + Directories: *dirs, + LogLevel: *logLevel, + IgnoreConfig: *ignoreConfig, + Depth: *recurseDepth, + }) if err != nil { log.Fatal(err) } diff --git a/pkg/app/app.go b/pkg/app/app.go index d1ab15b..9082386 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -8,20 +8,41 @@ import ( // The App struct is responsible to hold app-wide related entities. Currently // it has only the gui.Gui pointer for interface entity. type App struct { - Gui *gui.Gui + Gui *gui.Gui + Config *Config +} + +// SetupConfig is an assembler data to initiate a setup +type SetupConfig struct { + Directories []string + LogLevel string + IgnoreConfig bool + Depth int } // Setup will handle pre-required operations. It is designed to be a wrapper for // main method right now. -func Setup(directory, repoPattern, logLevel string) (*App, error) { +func Setup(setupConfig SetupConfig) (*App, error) { // initiate the app and give it initial values app := &App{} - setLogLevel(logLevel) + setLogLevel(setupConfig.LogLevel) var err error - directories := generateDirectories(directory, repoPattern) + app.Config, err = LoadConfiguration() + if err != nil { + // the error types and handling is not considered yer + log.Error(err) + return app, err + } + directories := make([]string, 0) + + if len(app.Config.Directories) <= 0 || setupConfig.IgnoreConfig { + directories = generateDirectories(setupConfig.Directories, setupConfig.Depth) + } else { + directories = generateDirectories(app.Config.Directories, setupConfig.Depth) + } // create a gui.Gui struct and set it as App's gui - app.Gui, err = gui.NewGui(directories) + app.Gui, err = gui.NewGui(app.Config.Mode, directories) if err != nil { // the error types and handling is not considered yer log.Error(err) diff --git a/pkg/app/config.go b/pkg/app/config.go new file mode 100644 index 0000000..94052e6 --- /dev/null +++ b/pkg/app/config.go @@ -0,0 +1,112 @@ +package app + +import ( + "os" + "path/filepath" + "runtime" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// Config type is the configuration entity of the application +type Config struct { + Mode string + Directories []string +} + +// config file stuff +var ( + configFileName = "config" + configFileExt = ".yml" + configType = "yaml" + appName = "gitbatch" + + configurationDirectory = filepath.Join(osConfigDirectory(), appName) + configFileAbsPath = filepath.Join(configurationDirectory, configFileName) +) + +// configuration items +var ( + modeKey = "mode" + modeKeyDefault = "fetch" + pathsKey = "paths" + pathsKeyDefault = []string{"."} +) + +// LoadConfiguration returns a Config struct is filled +func LoadConfiguration() (*Config, error) { + if err := initializeConfigurationManager(); err != nil { + return nil, err + } + if err := setDefaults(); err != nil { + return nil, err + } + if err := readConfiguration(); err != nil { + return nil, err + } + config := &Config{ + Mode: viper.GetString(modeKey), + Directories: viper.GetStringSlice(pathsKey), + } + return config, nil +} + +// set default configuration parameters +func setDefaults() error { + viper.SetDefault(modeKey, modeKeyDefault) + // viper.SetDefault(pathsKey, pathsKeyDefault) + return nil +} + +// read configuration from file +func readConfiguration() error { + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + // if file does not exist, simply create one + if _, err := os.Stat(configFileAbsPath + configFileExt); os.IsNotExist(err) { + os.MkdirAll(configurationDirectory, 0755) + os.Create(configFileAbsPath + configFileExt) + } else { + return err + } + // let's write defaults + if err := viper.WriteConfig(); err != nil { + return err + } + } + return nil +} + +// write configuration to a file +func writeConfiguration() error { + if err := viper.WriteConfig(); err != nil { + return err + } + return nil +} + +// initialize the configuration manager +func initializeConfigurationManager() error { + // config viper + viper.AddConfigPath(configurationDirectory) + viper.SetConfigName(configFileName) + viper.SetConfigType(configType) + + return nil +} + +// returns OS dependent config directory +func osConfigDirectory() (osConfigDirectory string) { + switch osname := runtime.GOOS; osname { + case "windows": + osConfigDirectory = os.Getenv("APPDATA") + case "darwin": + osConfigDirectory = os.Getenv("HOME") + "/Library/Application Support" + case "linux": + osConfigDirectory = os.Getenv("HOME") + "/.config" + default: + log.Warn("Operating system couldn't be recognized") + } + return osConfigDirectory +} diff --git a/pkg/app/files.go b/pkg/app/files.go index f6ae947..b6ec662 100644 --- a/pkg/app/files.go +++ b/pkg/app/files.go @@ -9,50 +9,88 @@ import ( log "github.com/sirupsen/logrus" ) -// generateDirectories is to find all the files in given path. This method +// generateDirectories returns poosible git repositories to pipe into git pkg's +// load function +func generateDirectories(directories []string, depth int) (gitDirectories []string) { + for i := 0; i <= depth; i++ { + nonrepos, repos := walkRecursive(directories, gitDirectories) + directories = nonrepos + gitDirectories = repos + } + return gitDirectories +} + +// returns given values, first search directories and second stands for possible +// git repositories. Call this func from a "for i := 0; i<depth; i++" loop +func walkRecursive(search, appendant []string) ([]string, []string) { + max := len(search) + for i := 0; i < max; i++ { + if i >= len(search) { + continue + } + // find possible repositories and remaining ones, b slice is possible ones + a, b, err := seperateDirectories(search[i]) + if err != nil { + log.WithFields(log.Fields{ + "directory": search[i], + }).Trace("Can't read directory") + continue + } + // since we started to search let's get rid of it and remove from search + // array + search[i] = search[len(search)-1] + search = search[:len(search)-1] + // lets append what we have found to continue recursion + search = append(search, a...) + appendant = append(appendant, b...) + } + return search, appendant +} + +// seperateDirectories is to find all the files in given path. This method // does not check if the given file is a valid git repositories -func generateDirectories(directory string, repoPattern string) (directories []string) { +func seperateDirectories(directory string) (directories, gitDirectories []string, err error) { files, err := ioutil.ReadDir(directory) - // can we read the directory? if err != nil { - log.Fatal(err) + log.WithFields(log.Fields{ + "directory": directory, + }).Trace("Can't read directory") + return directories, gitDirectories, nil } - - // filter according to a pattern - filteredFiles := filterDirectories(files, repoPattern) - - // now let's iterate over the our desired git directories - for _, f := range filteredFiles { + for _, f := range files { repo := directory + string(os.PathSeparator) + f.Name() file, err := os.Open(repo) - // if we cannot open it, simply continue to iteration and don't consider if err != nil { log.WithFields(log.Fields{ "file": file, - "directory": directory, + "directory": repo, }).Trace("Failed to open file in the directory") continue } dir, err := filepath.Abs(file.Name()) if err != nil { - log.Fatal(err) + return nil, nil, err + } + // with this approach, we ignore submodule or sub repositoreis in a git repository + _, err = os.Open(dir + string(os.PathSeparator) + ".git") + if err != nil { + directories = append(directories, dir) + } else { + gitDirectories = append(gitDirectories, dir) } - - // shaping our directory slice - directories = append(directories, dir) } - return directories + return directories, gitDirectories, nil } // takes a fileInfo slice and returns it with the ones matches with the -// repoPattern string -func filterDirectories(files []os.FileInfo, repoPattern string) []os.FileInfo { +// pattern string +func filterDirectories(files []os.FileInfo, pattern string) []os.FileInfo { var filteredRepos []os.FileInfo for _, f := range files { // it is just a simple filter - if strings.Contains(f.Name(), repoPattern) { + if strings.Contains(f.Name(), pattern) && f.Name() != ".git" { filteredRepos = append(filteredRepos, f) } else { continue diff --git a/pkg/git/add.go b/pkg/git/add.go new file mode 100644 index 0000000..6abb868 --- /dev/null +++ b/pkg/git/add.go @@ -0,0 +1,52 @@ +package git + +import ( + "errors" + "strings" + + log "github.com/sirupsen/logrus" +) + +var addCommand = "add" + +type AddOptions struct { + Update bool + Force bool + DryRun bool +} + +func (file *File) Add(option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) + args = append(args, file.Name) + if option.Update { + args = append(args, "--update") + } + if option.Force { + args = append(args, "--force") + } + if option.DryRun { + args = append(args, "--dry-run") + } + out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +func (entity *RepoEntity) AddAll(option AddOptions) error { + args := make([]string, 0) + args = append(args, addCommand) + if option.DryRun { + args = append(args, "--dry-run") + } + args = append(args, ".") + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} diff --git a/pkg/git/branch.go b/pkg/git/branch.go index 4f54ced..0fbfdf0 100644 --- a/pkg/git/branch.go +++ b/pkg/git/branch.go @@ -6,8 +6,8 @@ import ( "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" "regexp" - "strings" "strconv" + "strings" ) // Branch is the wrapper of go-git's Reference struct. In addition to that, it @@ -51,7 +51,7 @@ func (entity *RepoEntity) loadLocalBranches() error { pushables, err := RevList(entity, RevListOptions{ Ref1: "@{u}", Ref2: "HEAD", - }) + }) if err != nil { push = pushables[0] } else { @@ -60,7 +60,7 @@ func (entity *RepoEntity) loadLocalBranches() error { pullables, err := RevList(entity, RevListOptions{ Ref1: "HEAD", Ref2: "@{u}", - }) + }) if err != nil { pull = pullables[0] } else { @@ -129,12 +129,9 @@ func (entity *RepoEntity) Checkout(branch *Branch) error { entity.Commit = entity.Commits[0] entity.Branch = branch entity.RefreshPushPull() - // TODO: same code on 3 different occasion, maybe something wrong? // make this conditional on global scale - if err = entity.Remote.switchRemoteBranch(entity.Remote.Name + "/" + entity.Branch.Name); err != nil { - // probably couldn't find, but its ok. - log.Trace("Cannot find proper remote branch " + err.Error()) - return nil + if err := entity.Remote.SyncBranches(branch.Name); err != nil { + return err } return nil } @@ -163,7 +160,7 @@ func (entity *RepoEntity) RefreshPushPull() { pushables, err := RevList(entity, RevListOptions{ Ref1: "@{u}", Ref2: "HEAD", - }) + }) if err != nil { entity.Branch.Pushables = pushables[0] } else { @@ -172,7 +169,7 @@ func (entity *RepoEntity) RefreshPushPull() { pullables, err := RevList(entity, RevListOptions{ Ref1: "HEAD", Ref2: "@{u}", - }) + }) if err != nil { entity.Branch.Pullables = pullables[0] } else { @@ -187,7 +184,7 @@ func (entity *RepoEntity) pullDiffsToUpstream() ([]*Commit, error) { pullables, err := RevList(entity, RevListOptions{ Ref1: "HEAD", Ref2: "@{u}", - }) + }) if err != nil { // possibly found nothing or no upstream set } else { @@ -210,7 +207,7 @@ func (entity *RepoEntity) pushDiffsToUpstream() ([]string, error) { pushables, err := RevList(entity, RevListOptions{ Ref1: "@{u}", Ref2: "HEAD", - }) + }) if err != nil { return make([]string, 0), nil } diff --git a/pkg/git/commands.go b/pkg/git/commands.go index 0a1820b..b78b501 100644 --- a/pkg/git/commands.go +++ b/pkg/git/commands.go @@ -13,7 +13,7 @@ func GenericGitCommand(repoPath string, args []string) error { return nil } -// GenericGitCommand runs any git command with returning output +// GenericGitCommandWithOutput runs any git command with returning output func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) { out, err := helpers.RunCommandWithOutput(repoPath, "git", args) if err != nil { @@ -22,6 +22,15 @@ func GenericGitCommandWithOutput(repoPath string, args []string) (string, error) return helpers.TrimTrailingNewline(out), nil } +// GenericGitCommandWithErrorOutput runs any git command with returning output +func GenericGitCommandWithErrorOutput(repoPath string, args []string) (string, error) { + out, err := helpers.RunCommandWithOutput(repoPath, "git", args) + if err != nil { + return helpers.TrimTrailingNewline(out), err + } + return helpers.TrimTrailingNewline(out), nil +} + // GitShow is conventional git show command without any argument func GitShow(repoPath, hash string) string { args := []string{"show", hash} diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go index 1b3bb69..5dad021 100644 --- a/pkg/git/fetch.go +++ b/pkg/git/fetch.go @@ -6,17 +6,18 @@ import ( var fetchCommand = "fetch" +// FetchOptions defines the rules for fetch operation type FetchOptions struct { - // Name of the remote to fetch from. Defaults to origin. - RemoteName string - // Before fetching, remove any remote-tracking references that no longer - // exist on the remote. - Prune bool - // Show what would be done, without making any changes. - DryRun bool - // Force allows the fetch to update a local branch even when the remote - // branch does not descend from it. - Force bool + // Name of the remote to fetch from. Defaults to origin. + RemoteName string + // Before fetching, remove any remote-tracking references that no longer + // exist on the remote. + Prune bool + // Show what would be done, without making any changes. + DryRun bool + // Force allows the fetch to update a local branch even when the remote + // branch does not descend from it. + Force bool } // Fetch branches refs from one or more other repositories, along with the @@ -40,5 +41,6 @@ func Fetch(entity *RepoEntity, options FetchOptions) error { log.Warn("Error while fetching") return err } + entity.Refresh() return nil -}
\ No newline at end of file +} diff --git a/pkg/git/merge.go b/pkg/git/merge.go index f2a8fae..984bc4b 100644 --- a/pkg/git/merge.go +++ b/pkg/git/merge.go @@ -6,13 +6,14 @@ import ( var mergeCommand = "merge" +// MergeOptions defines the rules of a merge operation type MergeOptions struct { - // Name of the branch to merge with. - BranchName string - // Be verbose. - Verbose bool - // With true do not show a diffstat at the end of the merge. - NoStat bool + // Name of the branch to merge with. + BranchName string + // Be verbose. + Verbose bool + // With true do not show a diffstat at the end of the merge. + NoStat bool } // Merge incorporates changes from the named commits or branches into the @@ -33,5 +34,6 @@ func Merge(entity *RepoEntity, options MergeOptions) error { log.Warn("Error while merging") return err } + entity.Refresh() return nil -}
\ No newline at end of file +} diff --git a/pkg/git/remote.go b/pkg/git/remote.go index d1aeeab..62eb0db 100644 --- a/pkg/git/remote.go +++ b/pkg/git/remote.go @@ -22,9 +22,8 @@ func (entity *RepoEntity) NextRemote() error { } else { entity.Remote = entity.Remotes[currentRemoteIndex+1] } - // TODO: same code on 3 different occasion, maybe something wrong? - if err := entity.Remote.switchRemoteBranch(entity.Remote.Name + "/" + entity.Branch.Name); err != nil { - // probably couldn't find, but its ok. + if err := entity.Remote.SyncBranches(entity.Branch.Name); err != nil { + return err } return nil } @@ -37,9 +36,8 @@ func (entity *RepoEntity) PreviousRemote() error { } else { entity.Remote = entity.Remotes[currentRemoteIndex-1] } - // TODO: same code on 3 different occasion, maybe something wrong? - if err := entity.Remote.switchRemoteBranch(entity.Remote.Name + "/" + entity.Branch.Name); err != nil { - // probably couldn't find, but its ok. + if err := entity.Remote.SyncBranches(entity.Branch.Name); err != nil { + return err } return nil } @@ -81,3 +79,10 @@ func (entity *RepoEntity) loadRemotes() error { } return err } + +func (remote *Remote) SyncBranches(branchName string) error { + if err := remote.switchRemoteBranch(remote.Name + "/" + branchName); err != nil { + // probably couldn't find, but its ok. + } + return nil +} diff --git a/pkg/git/repository-sort.go b/pkg/git/repository-sort.go index 04cd9e7..de5f454 100644 --- a/pkg/git/repository-sort.go +++ b/pkg/git/repository-sort.go @@ -9,7 +9,7 @@ import ( type Alphabetical []*RepoEntity // Len is the interface implementation for Alphabetical sorting function -func (s Alphabetical) Len() int { return len(s) } +func (s Alphabetical) Len() int { return len(s) } // Swap is the interface implementation for Alphabetical sorting function func (s Alphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } @@ -48,7 +48,7 @@ func (s Alphabetical) Less(i, j int) bool { type LastModified []*RepoEntity // Len is the interface implementation for LastModified sorting function -func (s LastModified) Len() int { return len(s) } +func (s LastModified) Len() int { return len(s) } // Swap is the interface implementation for LastModified sorting function func (s LastModified) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/pkg/git/repository.go b/pkg/git/repository.go index e75281c..b127305 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -2,8 +2,8 @@ package git import ( "errors" - "time" "os" + "time" "github.com/isacikgoz/gitbatch/pkg/helpers" log "github.com/sirupsen/logrus" @@ -25,6 +25,7 @@ type RepoEntity struct { Remotes []*Remote Commit *Commit Commits []*Commit + Stasheds []*StashedItem State RepoState } @@ -85,11 +86,16 @@ func InitializeRepository(directory string) (entity *RepoEntity, err error) { entity.loadRemotes() // set the active branch to repositories HEAD entity.Branch = entity.getActiveBranch() + if err = entity.loadStashedItems(); err != nil { + // TODO: fix here. + } if len(entity.Remotes) > 0 { // TODO: tend to take origin/master as default entity.Remote = entity.Remotes[0] - // TODO: same code on 3 different occasion, maybe something wrong? - if err = entity.Remote.switchRemoteBranch(entity.Remote.Name + "/" + entity.Branch.Name); err != nil { + if entity.Branch == nil { + return nil, errors.New("Unable to find a valid branch") + } + if err = entity.Remote.SyncBranches(entity.Branch.Name); err != nil { // probably couldn't find, but its ok. } } else { @@ -118,6 +124,7 @@ func (entity *RepoEntity) Refresh() error { if err := entity.loadLocalBranches(); err != nil { return err } + entity.Branch.Clean = entity.isClean() entity.RefreshPushPull() if err := entity.loadCommits(); err != nil { return err @@ -125,5 +132,8 @@ func (entity *RepoEntity) Refresh() error { if err := entity.loadRemotes(); err != nil { return err } + if err := entity.loadStashedItems(); err != nil { + return err + } return nil } diff --git a/pkg/git/reset.go b/pkg/git/reset.go new file mode 100644 index 0000000..f93225c --- /dev/null +++ b/pkg/git/reset.go @@ -0,0 +1,55 @@ +package git + +import ( + "errors" + "strings" + + log "github.com/sirupsen/logrus" +) + +var resetCommand = "reset" + +// ResetOptions defines the rules of git reset command +type ResetOptions struct { + Hard bool + Merge bool + Keep bool +} + +// Reset is the wrapper of "git reset" command +func (file *File) Reset(option ResetOptions) error { + args := make([]string, 0) + args = append(args, resetCommand) + args = append(args, "--") + args = append(args, file.Name) + if option.Hard { + args = append(args, "--hard") + } + if option.Merge { + args = append(args, "--merge") + } + if option.Keep { + args = append(args, "--keep") + } + out, err := GenericGitCommandWithOutput(strings.TrimSuffix(file.AbsPath, file.Name), args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} + +// ResetAll resets the changes in a repository, should be used wise +func (entity *RepoEntity) ResetAll(option ResetOptions) error { + args := make([]string, 0) + args = append(args, resetCommand) + if option.Hard { + args = append(args, "--hard") + } + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while add command") + return errors.New(out + "\n" + err.Error()) + } + return nil +} diff --git a/pkg/git/rev-list.go b/pkg/git/rev-list.go index f9f45da..5debe47 100644 --- a/pkg/git/rev-list.go +++ b/pkg/git/rev-list.go @@ -9,11 +9,12 @@ import ( var revlistCommand = "rev-list" var hashLength = 40 +// RevListOptions defines the rules of rev-list func type RevListOptions struct { - // Ref1 is the first reference hash to link - Ref1 string - // Ref2 is the second reference hash to link - Ref2 string + // Ref1 is the first reference hash to link + Ref1 string + // Ref2 is the second reference hash to link + Ref2 string } // RevList returns the commit hashes that are links from the given commit(s). @@ -22,7 +23,7 @@ func RevList(entity *RepoEntity, options RevListOptions) ([]string, error) { args := make([]string, 0) args = append(args, revlistCommand) if len(options.Ref1) > 0 && len(options.Ref2) > 0 { - arg1 := options.Ref1+".."+options.Ref2 + arg1 := options.Ref1 + ".." + options.Ref2 args = append(args, arg1) } out, err := GenericGitCommandWithOutput(entity.AbsPath, args) @@ -39,4 +40,4 @@ func RevList(entity *RepoEntity, options RevListOptions) ([]string, error) { } } return hashes, nil -}
\ No newline at end of file +} diff --git a/pkg/git/stash.go b/pkg/git/stash.go new file mode 100644 index 0000000..66254c8 --- /dev/null +++ b/pkg/git/stash.go @@ -0,0 +1,95 @@ +package git + +import ( + "regexp" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" +) + +var stashCommand = "stash" + +// StashedItem holds the required fields for a stashed change +type StashedItem struct { + StashID int + BranchName string + Hash string + Description string + EntityPath string +} + +func stashGet(entity *RepoEntity, option string) string { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, option) + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while stash command") + return "?" + } + return out +} + +func (entity *RepoEntity) loadStashedItems() error { + entity.Stasheds = make([]*StashedItem, 0) + output := stashGet(entity, "list") + stashIDRegex := regexp.MustCompile(`stash@{[\d]+}:`) + stashIDRegexInt := regexp.MustCompile(`[\d]+`) + stashBranchRegex := regexp.MustCompile(`[\w]+: `) + stashHashRegex := regexp.MustCompile(`[\w]{7}`) + + stashlist := strings.Split(output, "\n") + for _, stashitem := range stashlist { + // find id + id := stashIDRegexInt.FindString(stashIDRegex.FindString(stashitem)) + i, err := strconv.Atoi(id) + if err != nil { + // probably something isn't right let's continue over this iteration + log.Trace("cannot initiate stashed item") + continue + } + // trim id section + trimmed := stashIDRegex.Split(stashitem, 2)[1] + + // find branch + stashBranchRegexMatch := stashBranchRegex.FindString(trimmed) + branchName := stashBranchRegexMatch[:len(stashBranchRegexMatch)-2] + + // trim branch section + trimmed = stashBranchRegex.Split(trimmed, 2)[1] + hash := stashHashRegex.FindString(trimmed) + + // trim hash + desc := stashHashRegex.Split(trimmed, 2)[1][1:] + + entity.Stasheds = append(entity.Stasheds, &StashedItem{ + StashID: i, + BranchName: branchName, + Hash: hash, + Description: desc, + EntityPath: entity.AbsPath, + }) + } + return nil +} + +// Stash is the wrapper of convetional "git stash" command +func (entity *RepoEntity) Stash() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + + output, err = GenericGitCommandWithErrorOutput(entity.AbsPath, args) + entity.Refresh() + return output, err +} + +// Pop is the wrapper of "git stash pop" command that used for a file +func (stashedItem *StashedItem) Pop() (output string, err error) { + args := make([]string, 0) + args = append(args, stashCommand) + args = append(args, "pop") + args = append(args, "stash@{"+strconv.Itoa(stashedItem.StashID)+"}") + output, err = GenericGitCommandWithErrorOutput(stashedItem.EntityPath, args) + return output, err +} diff --git a/pkg/git/status.go b/pkg/git/status.go new file mode 100644 index 0000000..8db2ec7 --- /dev/null +++ b/pkg/git/status.go @@ -0,0 +1,81 @@ +package git + +import ( + "os" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +var statusCommand = "status" + +// File represents the status of a file in an index or work tree +type File struct { + Name string + AbsPath string + X FileStatus + Y FileStatus +} + +// FileStatus is the short representation of state of a file +type FileStatus rune + +var ( + // StatusNotupdated says file not updated + StatusNotupdated FileStatus = ' ' + // StatusModified says file is modifed + StatusModified FileStatus = 'M' + // StatusAdded says file is added to index + StatusAdded FileStatus = 'A' + // StatusDeleted says file is deleted + StatusDeleted FileStatus = 'D' + // StatusRenamed says file is renamed + StatusRenamed FileStatus = 'R' + // StatusCopied says file is copied + StatusCopied FileStatus = 'C' + // StatusUpdated says file is updated + StatusUpdated FileStatus = 'U' + // StatusUntracked says file is untraced + StatusUntracked FileStatus = '?' + // StatusIgnored says file is ignored + StatusIgnored FileStatus = '!' +) + +func shortStatus(entity *RepoEntity, option string) string { + args := make([]string, 0) + args = append(args, statusCommand) + args = append(args, option) + args = append(args, "--short") + out, err := GenericGitCommandWithOutput(entity.AbsPath, args) + if err != nil { + log.Warn("Error while status command") + return "?" + } + return out +} + +// LoadFiles function simply commands a git status and collects output in a +// structured way +func (entity *RepoEntity) LoadFiles() ([]*File, error) { + files := make([]*File, 0) + output := shortStatus(entity, "--untracked-files=all") + if len(output) == 0 { + return files, nil + } + fileslist := strings.Split(output, "\n") + for _, file := range fileslist { + x := rune(file[0]) + y := rune(file[1]) + relativePathRegex := regexp.MustCompile(`[(\w|/|.|\-)]+`) + path := relativePathRegex.FindString(file[2:]) + + files = append(files, &File{ + Name: path, + AbsPath: entity.AbsPath + string(os.PathSeparator) + path, + X: FileStatus(x), + Y: FileStatus(y), + }) + } + return files, nil +} diff --git a/pkg/gui/branchview.go b/pkg/gui/branchview.go index b6a2b43..c955cf0 100644 --- a/pkg/gui/branchview.go +++ b/pkg/gui/branchview.go @@ -83,4 +83,4 @@ func (gui *Gui) checkoutFollowUp(g *gocui.Gui, entity *git.RepoEntity) (err erro return err } return nil -}
\ No newline at end of file +} diff --git a/pkg/gui/gui-navigate.go b/pkg/gui/gui-navigate.go deleted file mode 100644 index 56d96de..0000000 --- a/pkg/gui/gui-navigate.go +++ /dev/null @@ -1,54 +0,0 @@ -package gui - -import ( - log "github.com/sirupsen/logrus" - "github.com/jroimartin/gocui" -) - -// focus to next view -func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { - var focusedViewName string - if v == nil || v.Name() == mainViews[len(mainViews)-1].Name { - focusedViewName = mainViews[0].Name - } else { - for i := range mainViews { - if v.Name() == mainViews[i].Name { - focusedViewName = mainViews[i+1].Name - break - } - if i == len(mainViews)-1 { - return nil - } - } - } - if _, err := g.SetCurrentView(focusedViewName); err != nil { - log.Warn("Loading view cannot be focused.") - return nil - } - gui.updateKeyBindingsView(g, focusedViewName) - return nil -} - -// focus to previous view -func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { - var focusedViewName string - if v == nil || v.Name() == mainViews[0].Name { - focusedViewName = mainViews[len(mainViews)-1].Name - } else { - for i := range mainViews { - if v.Name() == mainViews[i].Name { - focusedViewName = mainViews[i-1].Name - break - } - if i == len(mainViews)-1 { - return nil - } - } - } - if _, err := g.SetCurrentView(focusedViewName); err != nil { - log.Warn("Loading view cannot be focused.") - return nil - } - gui.updateKeyBindingsView(g, focusedViewName) - return nil -} diff --git a/pkg/gui/gui-util.go b/pkg/gui/gui-util.go index 2ae8cae..2462723 100644 --- a/pkg/gui/gui-util.go +++ b/pkg/gui/gui-util.go @@ -1,11 +1,10 @@ package gui import ( - "sort" - "github.com/isacikgoz/gitbatch/pkg/git" "github.com/isacikgoz/gitbatch/pkg/helpers" "github.com/jroimartin/gocui" + log "github.com/sirupsen/logrus" ) // refreshes the side views of the application for given git.RepoEntity struct @@ -26,18 +25,70 @@ func (gui *Gui) refreshViews(g *gocui.Gui, entity *git.RepoEntity) error { return err } +// focus to next view +func (gui *Gui) nextViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFeature) error { + var focusedViewName string + if v == nil || v.Name() == group[len(group)-1].Name { + focusedViewName = group[0].Name + } else { + for i := range group { + if v.Name() == group[i].Name { + focusedViewName = group[i+1].Name + break + } + if i == len(group)-1 { + return nil + } + } + } + if _, err := g.SetCurrentView(focusedViewName); err != nil { + log.WithFields(log.Fields{ + "view": focusedViewName, + }).Warn("View cannot be focused.") + return nil + } + gui.updateKeyBindingsView(g, focusedViewName) + return nil +} + +// focus to previous view +func (gui *Gui) previousViewOfGroup(g *gocui.Gui, v *gocui.View, group []viewFeature) error { + var focusedViewName string + if v == nil || v.Name() == group[0].Name { + focusedViewName = group[len(group)-1].Name + } else { + for i := range group { + if v.Name() == group[i].Name { + focusedViewName = group[i-1].Name + break + } + if i == len(group)-1 { + return nil + } + } + } + if _, err := g.SetCurrentView(focusedViewName); err != nil { + log.WithFields(log.Fields{ + "view": focusedViewName, + }).Warn("View cannot be focused.") + return nil + } + gui.updateKeyBindingsView(g, focusedViewName) + return nil +} + // siwtch the app mode // TODO: switching can be made with conventional iteration func (gui *Gui) switchMode(g *gocui.Gui, v *gocui.View) error { - switch mode := gui.State.Mode.ModeID; mode { - case FetchMode: - gui.State.Mode = pullMode - case PullMode: - gui.State.Mode = mergeMode - case MergeMode: - gui.State.Mode = fetchMode - default: - gui.State.Mode = fetchMode + for i, mode := range modes { + if mode == gui.State.Mode { + if i == len(modes)-1 { + gui.State.Mode = modes[0] + break + } + gui.State.Mode = modes[i+1] + break + } } gui.updateKeyBindingsView(g, mainViewFeature.Name) return nil @@ -110,35 +161,14 @@ func writeRightHandSide(v *gocui.View, text string, cx, cy int) error { return nil } -// sortByName sorts the repositories by A to Z order -func (gui *Gui) sortByName(g *gocui.Gui, v *gocui.View) error { - sort.Sort(git.Alphabetical(gui.State.Repositories)) - gui.refreshAfterSort(g) - return nil -} - -// sortByMod sorts the repositories according to last modifed date -// the top element will be the last modified -func (gui *Gui) sortByMod(g *gocui.Gui, v *gocui.View) error { - sort.Sort(git.LastModified(gui.State.Repositories)) - gui.refreshAfterSort(g) - return nil -} - -// utility function that refreshes main and side views after that -func (gui *Gui) refreshAfterSort(g *gocui.Gui) error { - gui.refreshMain(g) - entity := gui.getSelectedRepository() - gui.refreshViews(g, entity) - return nil -} - // cursor down acts like half-page down for faster scrolling func (gui *Gui) fastCursorDown(g *gocui.Gui, v *gocui.View) error { if v != nil { ox, oy := v.Origin() _, vy := v.Size() - + if len(v.BufferLines())+len(v.ViewBufferLines()) <= vy+oy || len(v.ViewBufferLines()) < vy { + return nil + } // TODO: do something when it hits bottom if err := v.SetOrigin(ox, oy+vy/2); err != nil { return err diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 0598383..0cf9458 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -41,15 +41,15 @@ type mode struct { } // ModeID is the mode indicator for the gui -type ModeID int8 +type ModeID string const ( // FetchMode puts the gui in fetch state - FetchMode ModeID = 0 + FetchMode ModeID = "fetch" // PullMode puts the gui in pull state - PullMode ModeID = 1 + PullMode ModeID = "pull" // MergeMode puts the gui in merge state - MergeMode ModeID = 2 + MergeMode ModeID = "merge" ) var ( @@ -70,10 +70,11 @@ var ( mergeMode = mode{ModeID: MergeMode, DisplayString: "Merge", CommandString: "merge"} mainViews = []viewFeature{mainViewFeature, remoteViewFeature, remoteBranchViewFeature, branchViewFeature, commitViewFeature} + modes = []mode{fetchMode, pullMode, mergeMode} ) // NewGui creates a Gui opject and fill it's state related entites -func NewGui(directoies []string) (*Gui, error) { +func NewGui(mode string, directoies []string) (*Gui, error) { initialState := guiState{ Directories: directoies, Mode: fetchMode, @@ -82,6 +83,12 @@ func NewGui(directoies []string) (*Gui, error) { gui := &Gui{ State: initialState, } + for _, m := range modes { + if string(m.ModeID) == mode { + gui.State.Mode = m + break + } + } return gui, nil } @@ -194,6 +201,22 @@ func (gui *Gui) layout(g *gocui.Gui) error { return nil } +// focus to next view +func (gui *Gui) nextMainView(g *gocui.Gui, v *gocui.View) error { + if err := gui.nextViewOfGroup(g, v, mainViews); err != nil { + return err + } + return nil +} + +// focus to previous view +func (gui *Gui) previousMainView(g *gocui.Gui, v *gocui.View) error { + if err := gui.previousViewOfGroup(g, v, mainViews); err != nil { + return err + } + return nil +} + // quit from the gui and end its loop func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index b2622bc..1a6d6c9 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -43,7 +43,7 @@ func (gui *Gui) generateKeybindings() error { View: view.Name, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, - Handler: gui.previousView, + Handler: gui.previousMainView, Display: "←", Description: "Previous Panel", Vital: false, @@ -51,7 +51,7 @@ func (gui *Gui) generateKeybindings() error { View: view.Name, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, - Handler: gui.nextView, + Handler: gui.nextMainView, Display: "→", Description: "Next Panel", Vital: false, @@ -59,7 +59,7 @@ func (gui *Gui) generateKeybindings() error { View: view.Name, Key: 'l', Modifier: gocui.ModNone, - Handler: gui.nextView, + Handler: gui.nextMainView, Display: "l", Description: "Previous Panel", Vital: false, @@ -67,7 +67,7 @@ func (gui *Gui) generateKeybindings() error { View: view.Name, Key: 'h', Modifier: gocui.ModNone, - Handler: gui.previousView, + Handler: gui.previousMainView, Display: "h", Description: "Next Panel", Vital: false, @@ -77,7 +77,143 @@ func (gui *Gui) generateKeybindings() error { gui.KeyBindings = append(gui.KeyBindings, binding) } } + // Statusviews common keybindings + for _, view := range statusViews { + statusKeybindings := []*KeyBinding{ + { + View: view.Name, + Key: 'c', + Modifier: gocui.ModNone, + Handler: gui.closeStatusView, + Display: "c", + Description: "Close/Cancel", + Vital: true, + }, { + View: view.Name, + Key: gocui.KeyArrowLeft, + Modifier: gocui.ModNone, + Handler: gui.previousStatusView, + Display: "←", + Description: "Previous Panel", + Vital: false, + }, { + View: view.Name, + Key: gocui.KeyArrowRight, + Modifier: gocui.ModNone, + Handler: gui.nextStatusView, + Display: "→", + Description: "Next Panel", + Vital: false, + }, { + View: view.Name, + Key: 'l', + Modifier: gocui.ModNone, + Handler: gui.nextStatusView, + Display: "l", + Description: "Previous Panel", + Vital: false, + }, { + View: view.Name, + Key: 'h', + Modifier: gocui.ModNone, + Handler: gui.previousStatusView, + Display: "h", + Description: "Next Panel", + Vital: false, + }, { + View: view.Name, + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.statusCursorUp, + Display: "↑", + Description: "Up", + Vital: false, + }, { + View: view.Name, + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.statusCursorDown, + Display: "↓", + Description: "Down", + Vital: false, + }, { + View: view.Name, + Key: 'k', + Modifier: gocui.ModNone, + Handler: gui.statusCursorUp, + Display: "k", + Description: "Up", + Vital: false, + }, { + View: view.Name, + Key: 'j', + Modifier: gocui.ModNone, + Handler: gui.statusCursorDown, + Display: "j", + Description: "Down", + Vital: false, + }, { + View: view.Name, + Key: 't', + Modifier: gocui.ModNone, + Handler: gui.stashChanges, + Display: "t", + Description: "Save to Stash", + Vital: true, + }, + } + for _, binding := range statusKeybindings { + gui.KeyBindings = append(gui.KeyBindings, binding) + } + } individualKeybindings := []*KeyBinding{ + // stash view + { + View: stashViewFeature.Name, + Key: 'p', + Modifier: gocui.ModNone, + Handler: gui.popStash, + Display: "p", + Description: "Pop Item", + Vital: true, + }, + // staged view + { + View: stageViewFeature.Name, + Key: 'r', + Modifier: gocui.ModNone, + Handler: gui.resetChanges, + Display: "r", + Description: "Reset Item", + Vital: true, + }, { + View: stageViewFeature.Name, + Key: gocui.KeyCtrlR, + Modifier: gocui.ModNone, + Handler: gui.resetAllChanges, + Display: "ctrl+r", + Description: "Reset All Items", + Vital: true, + }, + // unstaged view + { + View: unstageViewFeature.Name, + Key: 'a', + Modifier: gocui.ModNone, + Handler: gui.addChanges, + Display: "a", + Description: "Add Item", + Vital: true, + }, { + View: unstageViewFeature.Name, + Key: gocui.KeyCtrlA, + Modifier: gocui.ModNone, + Handler: gui.addAllChanges, + Display: "ctrl+a", + Description: "Add All Items", + Vital: true, + }, + // Main view controls { View: mainViewFeature.Name, Key: gocui.KeyArrowUp, @@ -167,6 +303,14 @@ func (gui *Gui) generateKeybindings() error { Description: "Sort repositories by Modification date", Vital: false, }, { + View: mainViewFeature.Name, + Key: 's', + Modifier: gocui.ModNone, + Handler: gui.openStatusView, + Display: "s", + Description: "Open Status", + Vital: true, + }, { View: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, @@ -174,8 +318,8 @@ func (gui *Gui) generateKeybindings() error { Display: "ctrl + c", Description: "Force application to quit", Vital: false, - }, - // Branch View Controls + }, + // Branch View Controls { View: branchViewFeature.Name, Key: gocui.KeyArrowDown, @@ -209,7 +353,7 @@ func (gui *Gui) generateKeybindings() error { Description: "Up", Vital: false, }, - // Remote View Controls + // Remote View Controls { View: remoteViewFeature.Name, Key: gocui.KeyArrowDown, @@ -243,7 +387,7 @@ func (gui *Gui) generateKeybindings() error { Description: "Up", Vital: false, }, - // Remote Branch View Controls + // Remote Branch View Controls { View: remoteBranchViewFeature.Name, Key: gocui.KeyArrowDown, @@ -276,8 +420,16 @@ func (gui *Gui) generateKeybindings() error { Display: "k", Description: "Up", Vital: false, + }, { + View: remoteBranchViewFeature.Name, + Key: 's', + Modifier: gocui.ModNone, + Handler: gui.syncRemoteBranch, + Display: "s", + Description: "Synch with Remote", + Vital: true, }, - // Commit View Controls + // Commit View Controls { View: commitViewFeature.Name, Key: gocui.KeyArrowDown, @@ -286,7 +438,7 @@ func (gui *Gui) generateKeybindings() error { Display: "↓", Description: "Iterate over commits", Vital: false, - },{ + }, { View: commitViewFeature.Name, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, @@ -310,7 +462,7 @@ func (gui *Gui) generateKeybindings() error { Display: "k", Description: "Up", Vital: false, - },{ + }, { View: commitViewFeature.Name, Key: 'd', Modifier: gocui.ModNone, @@ -318,8 +470,8 @@ func (gui *Gui) generateKeybindings() error { Display: "d", Description: "Show commit diff", Vital: true, - }, - // Diff View Controls + }, + // Diff View Controls { View: commitDiffViewFeature.Name, Key: 'c', @@ -360,8 +512,8 @@ func (gui *Gui) generateKeybindings() error { Display: "j", Description: "Page down", Vital: false, - }, - // Application Controls + }, + // Application Controls { View: cheatSheetViewFeature.Name, Key: 'c', @@ -402,8 +554,8 @@ func (gui *Gui) generateKeybindings() error { Display: "j", Description: "Down", Vital: false, - }, - // Error View + }, + // Error View { View: errorViewFeature.Name, Key: 'c', @@ -412,7 +564,39 @@ func (gui *Gui) generateKeybindings() error { Display: "c", Description: "close/cancel", Vital: true, - }, + }, { + View: errorViewFeature.Name, + Key: gocui.KeyArrowUp, + Modifier: gocui.ModNone, + Handler: gui.fastCursorUp, + Display: "↑", + Description: "Up", + Vital: true, + }, { + View: errorViewFeature.Name, + Key: gocui.KeyArrowDown, + Modifier: gocui.ModNone, + Handler: gui.fastCursorDown, + Display: "↓", + Description: "Down", + Vital: true, + }, { + View: errorViewFeature.Name, + Key: 'k', + Modifier: gocui.ModNone, + Handler: gui.fastCursorUp, + Display: "k", + Description: "Up", + Vital: false, + }, { + View: errorViewFeature.Name, + Key: 'j', + Modifier: gocui.ModNone, + Handler: gui.fastCursorDown, + Display: "j", + Description: "Down", + Vital: false, + }, } for _, binding := range individualKeybindings { gui.KeyBindings = append(gui.KeyBindings, binding) diff --git a/pkg/gui/mainview.go b/pkg/gui/mainview.go index 58f8422..b421775 100644 --- a/pkg/gui/mainview.go +++ b/pkg/gui/mainview.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "sort" "github.com/isacikgoz/gitbatch/pkg/git" "github.com/isacikgoz/gitbatch/pkg/queue" @@ -36,6 +37,19 @@ func (gui *Gui) fillMain(g *gocui.Gui) error { return nil } +// refresh the main view and re-render the repository representations +func (gui *Gui) refreshMain(g *gocui.Gui) error { + mainView, err := g.View(mainViewFeature.Name) + if err != nil { + return err + } + mainView.Clear() + for _, r := range gui.State.Repositories { + fmt.Fprintln(mainView, gui.displayString(r)) + } + return nil +} + // moves the cursor downwards for the main view and if it goes to bottom it // prevents from going further func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { @@ -135,12 +149,12 @@ func (gui *Gui) removeFromQueue(entity *git.RepoEntity) error { func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error { r := gui.getSelectedRepository() if r.State == git.Available || r.State == git.Success { - if err := gui.addToQueue(r); err !=nil { + if err := gui.addToQueue(r); err != nil { return err } } else if r.State == git.Queued { - if err := gui.removeFromQueue(r); err !=nil { - return err + if err := gui.removeFromQueue(r); err != nil { + return err } } else { return nil @@ -154,7 +168,7 @@ func (gui *Gui) markRepository(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error { for _, r := range gui.State.Repositories { if r.State == git.Available || r.State == git.Success { - if err := gui.addToQueue(r); err !=nil { + if err := gui.addToQueue(r); err != nil { return err } } else { @@ -170,7 +184,7 @@ func (gui *Gui) markAllRepositories(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) unmarkAllRepositories(g *gocui.Gui, v *gocui.View) error { for _, r := range gui.State.Repositories { if r.State == git.Queued { - if err := gui.removeFromQueue(r); err !=nil { + if err := gui.removeFromQueue(r); err != nil { return err } } else { @@ -181,15 +195,25 @@ func (gui *Gui) unmarkAllRepositories(g *gocui.Gui, v *gocui.View) error { return nil } -// refresh the main view and re-render the repository representations -func (gui *Gui) refreshMain(g *gocui.Gui) error { - mainView, err := g.View(mainViewFeature.Name) - if err != nil { - return err - } - mainView.Clear() - for _, r := range gui.State.Repositories { - fmt.Fprintln(mainView, gui.displayString(r)) - } +// sortByName sorts the repositories by A to Z order +func (gui *Gui) sortByName(g *gocui.Gui, v *gocui.View) error { + sort.Sort(git.Alphabetical(gui.State.Repositories)) + gui.refreshAfterSort(g) + return nil +} + +// sortByMod sorts the repositories according to last modifed date +// the top element will be the last modified +func (gui *Gui) sortByMod(g *gocui.Gui, v *gocui.View) error { + sort.Sort(git.LastModified(gui.State.Repositories)) + gui.refreshAfterSort(g) + return nil +} + +// utility function that refreshes main and side views after that +func (gui *Gui) refreshAfterSort(g *gocui.Gui) error { + gui.refreshMain(g) + entity := gui.getSelectedRepository() + gui.refreshViews(g, entity) return nil } diff --git a/pkg/gui/remotebranchview.go b/pkg/gui/remotebranchview.go index eb65d55..6c58565 100644 --- a/pkg/gui/remotebranchview.go +++ b/pkg/gui/remotebranchview.go @@ -38,6 +38,26 @@ func (gui *Gui) updateRemoteBranches(g *gocui.Gui, entity *git.RepoEntity) error } // iteration handler for the remotebranchview +func (gui *Gui) syncRemoteBranch(g *gocui.Gui, v *gocui.View) error { + var err error + entity := gui.getSelectedRepository() + if err = git.Fetch(entity, git.FetchOptions{ + RemoteName: entity.Remote.Name, + Prune: true, + }); err != nil { + return err + } + // have no idea why this works.. + // some time need to fix, movement aint bad huh? + gui.nextRemote(g, v) + gui.previousRemote(g, v) + if err = gui.updateRemoteBranches(g, entity); err != nil { + return err + } + return nil +} + +// iteration handler for the remotebranchview func (gui *Gui) nextRemoteBranch(g *gocui.Gui, v *gocui.View) error { var err error entity := gui.getSelectedRepository() diff --git a/pkg/gui/remotesview.go b/pkg/gui/remotesview.go index 6f31265..55366b2 100644 --- a/pkg/gui/remotesview.go +++ b/pkg/gui/remotesview.go @@ -72,4 +72,4 @@ func (gui *Gui) remoteChangeFollowUp(g *gocui.Gui, entity *git.RepoEntity) (err return err } return nil -}
\ No newline at end of file +} diff --git a/pkg/gui/stagedview.go b/pkg/gui/stagedview.go new file mode 100644 index 0000000..3e83a96 --- /dev/null +++ b/pkg/gui/stagedview.go @@ -0,0 +1,84 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +// staged view +func (gui *Gui) openStageView(g *gocui.Gui) error { + maxX, maxY := g.Size() + + v, err := g.SetView(stageViewFeature.Name, 6, 5, maxX/2-1, int(0.75*float32(maxY))-1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = stageViewFeature.Title + } + entity := gui.getSelectedRepository() + if err := refreshStagedView(g, entity); err != nil { + return err + } + gui.updateKeyBindingsView(g, stageViewFeature.Name) + if _, err := g.SetCurrentView(stageViewFeature.Name); err != nil { + return err + } + return nil +} + +func (gui *Gui) resetChanges(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + files, _, err := generateFileLists(entity) + if err != nil { + return err + } + if len(files) <= 0 { + return nil + } + _, cy := v.Cursor() + _, oy := v.Origin() + if err := files[cy+oy].Reset(git.ResetOptions{}); err != nil { + return err + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + return nil +} + +func (gui *Gui) resetAllChanges(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + if err := entity.ResetAll(git.ResetOptions{}); err != nil { + return err + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + return nil +} + +// refresh the main view and re-render the repository representations +func refreshStagedView(g *gocui.Gui, entity *git.RepoEntity) error { + stageView, err := g.View(stageViewFeature.Name) + if err != nil { + return err + } + stageView.Clear() + _, cy := stageView.Cursor() + _, oy := stageView.Origin() + files, _, err := generateFileLists(entity) + if err != nil { + return err + } + for i, file := range files { + var prefix string + if i == cy+oy { + prefix = prefix + selectionIndicator + } + fmt.Fprintf(stageView, "%s%s%s %s\n", prefix, green.Sprint(string(file.X)), red.Sprint(string(file.Y)), file.Name) + } + return nil +} diff --git a/pkg/gui/stashview.go b/pkg/gui/stashview.go new file mode 100644 index 0000000..cfdc104 --- /dev/null +++ b/pkg/gui/stashview.go @@ -0,0 +1,89 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +// stash view +func (gui *Gui) openStashView(g *gocui.Gui) error { + maxX, maxY := g.Size() + + v, err := g.SetView(stashViewFeature.Name, 6, int(0.75*float32(maxY)), maxX-6, maxY-3) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = stashViewFeature.Title + } + entity := gui.getSelectedRepository() + if err := refreshStashView(g, entity); err != nil { + return err + } + return nil +} + +// +func (gui *Gui) stashChanges(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + output, err := entity.Stash() + if err != nil { + if err = gui.openErrorView(g, output, + "You should manually resolve this issue", + stashViewFeature.Name); err != nil { + return err + } + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + return nil +} + +// +func (gui *Gui) popStash(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + _, oy := v.Origin() + _, cy := v.Cursor() + if len(entity.Stasheds) <= 0 { + return nil + } + stashedItem := entity.Stasheds[oy+cy] + output, err := stashedItem.Pop() + if err != nil { + if err = gui.openErrorView(g, output, + "You should manually resolve this issue", + stashViewFeature.Name); err != nil { + return err + } + } + if err := entity.Refresh(); err != nil { + return err + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + return nil +} + +// refresh the main view and re-render the repository representations +func refreshStashView(g *gocui.Gui, entity *git.RepoEntity) error { + stashView, err := g.View(stashViewFeature.Name) + if err != nil { + return err + } + stashView.Clear() + _, cy := stashView.Cursor() + _, oy := stashView.Origin() + stashedItems := entity.Stasheds + for i, stashedItem := range stashedItems { + var prefix string + if i == cy+oy { + prefix = prefix + selectionIndicator + } + fmt.Fprintf(stashView, "%s%d %s: %s (%s)\n", prefix, stashedItem.StashID, cyan.Sprint(stashedItem.BranchName), stashedItem.Description, cyan.Sprint(stashedItem.Hash)) + } + return nil +} diff --git a/pkg/gui/statusview.go b/pkg/gui/statusview.go new file mode 100644 index 0000000..d95a56f --- /dev/null +++ b/pkg/gui/statusview.go @@ -0,0 +1,174 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +var ( + statusHeaderViewFeature = viewFeature{Name: "status-header", Title: " Status Header "} + // statusViewFeature = viewFeature{Name: "status", Title: " Status "} + stageViewFeature = viewFeature{Name: "staged", Title: " Staged "} + unstageViewFeature = viewFeature{Name: "unstaged", Title: " Unstaged "} + stashViewFeature = viewFeature{Name: "stash", Title: " Stash "} + + statusViews = []viewFeature{stageViewFeature, unstageViewFeature, stashViewFeature} +) + +// open the status layout +func (gui *Gui) openStatusView(g *gocui.Gui, v *gocui.View) error { + gui.openStatusHeaderView(g) + gui.openStageView(g) + gui.openUnStagedView(g) + gui.openStashView(g) + return nil +} + +// focus to next view +func (gui *Gui) nextStatusView(g *gocui.Gui, v *gocui.View) error { + if err := gui.nextViewOfGroup(g, v, statusViews); err != nil { + return err + } + return nil +} + +// focus to previous view +func (gui *Gui) previousStatusView(g *gocui.Gui, v *gocui.View) error { + if err := gui.previousViewOfGroup(g, v, statusViews); err != nil { + return err + } + return nil +} + +// moves the cursor downwards for the main view and if it goes to bottom it +// prevents from going further +func (gui *Gui) statusCursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + ox, oy := v.Origin() + ly := len(v.BufferLines()) - 2 // why magic number? have no idea + + // if we are at the end we just return + if cy+oy == ly { + return nil + } + if err := v.SetCursor(cx, cy+1); err != nil { + + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + entity := gui.getSelectedRepository() + if err := refreshStatusView(v.Name(), g, entity); err != nil { + return err + } + } + return nil +} + +// moves the cursor upwards for the main view +func (gui *Gui) statusCursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + entity := gui.getSelectedRepository() + if err := refreshStatusView(v.Name(), g, entity); err != nil { + return err + } + } + return nil +} + +// header og the status layout +func (gui *Gui) openStatusHeaderView(g *gocui.Gui) error { + maxX, _ := g.Size() + entity := gui.getSelectedRepository() + v, err := g.SetView(statusHeaderViewFeature.Name, 6, 2, maxX-6, 4) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(v, entity.AbsPath) + // v.Frame = false + v.Wrap = true + } + return nil +} + +// close the opened stat views +func (gui *Gui) closeStatusView(g *gocui.Gui, v *gocui.View) error { + if err := g.DeleteView(stashViewFeature.Name); err != nil { + return err + } + if err := g.DeleteView(unstageViewFeature.Name); err != nil { + return err + } + if err := g.DeleteView(stageViewFeature.Name); err != nil { + return err + } + if err := g.DeleteView(statusHeaderViewFeature.Name); err != nil { + return err + } + if _, err := g.SetCurrentView(mainViewFeature.Name); err != nil { + return err + } + entity := gui.getSelectedRepository() + if err := gui.refreshMain(g); err != nil { + return err + } + if err := gui.refreshViews(g, entity); err != nil { + return err + } + gui.updateKeyBindingsView(g, mainViewFeature.Name) + return nil +} + +func generateFileLists(entity *git.RepoEntity) (staged, unstaged []*git.File, err error) { + files, err := entity.LoadFiles() + if err != nil { + return nil, nil, err + } + for _, file := range files { + if file.X != git.StatusNotupdated && file.X != git.StatusUntracked && file.X != git.StatusIgnored && file.X != git.StatusUpdated { + staged = append(staged, file) + } + if file.Y != git.StatusNotupdated { + unstaged = append(unstaged, file) + } + } + return staged, unstaged, err +} + +func refreshStatusView(viewName string, g *gocui.Gui, entity *git.RepoEntity) error { + switch viewName { + case stageViewFeature.Name: + if err := refreshStagedView(g, entity); err != nil { + return err + } + case unstageViewFeature.Name: + if err := refreshUnstagedView(g, entity); err != nil { + return err + } + case stashViewFeature.Name: + if err := refreshStashView(g, entity); err != nil { + return err + } + } + return nil +} + +func refreshAllStatusView(g *gocui.Gui, entity *git.RepoEntity) error { + for _, v := range statusViews { + if err := refreshStatusView(v.Name, g, entity); err != nil { + return err + } + } + return nil +} diff --git a/pkg/gui/textstyle.go b/pkg/gui/textstyle.go index 554a590..50bc945 100644 --- a/pkg/gui/textstyle.go +++ b/pkg/gui/textstyle.go @@ -45,7 +45,7 @@ var ( keyBindingSeperator = "░" selectionIndicator = ws + string(green.Sprint("→")) + ws - tab = ws + tab = ws ) // this function handles the render and representation of the repository @@ -56,11 +56,11 @@ func (gui *Gui) displayString(entity *git.RepoEntity) string { repoName := "" if entity.Branch.Pushables != "?" { - prefix = prefix + pushable + ws + entity.Branch.Pushables + - ws + pullable + ws + entity.Branch.Pullables + prefix = prefix + pushable + ws + entity.Branch.Pushables + + ws + pullable + ws + entity.Branch.Pullables } else { - prefix = prefix + pushable + ws + yellow.Sprint(entity.Branch.Pushables) + - ws + pullable + ws + yellow.Sprint(entity.Branch.Pullables) + prefix = prefix + pushable + ws + yellow.Sprint(entity.Branch.Pushables) + + ws + pullable + ws + yellow.Sprint(entity.Branch.Pullables) } selectedEntity := gui.getSelectedRepository() diff --git a/pkg/gui/unstagedview.go b/pkg/gui/unstagedview.go new file mode 100644 index 0000000..93082cc --- /dev/null +++ b/pkg/gui/unstagedview.go @@ -0,0 +1,80 @@ +package gui + +import ( + "fmt" + + "github.com/isacikgoz/gitbatch/pkg/git" + "github.com/jroimartin/gocui" +) + +// not staged view +func (gui *Gui) openUnStagedView(g *gocui.Gui) error { + maxX, maxY := g.Size() + + v, err := g.SetView(unstageViewFeature.Name, maxX/2+1, 5, maxX-6, int(0.75*float32(maxY))-1) + if err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = unstageViewFeature.Title + } + entity := gui.getSelectedRepository() + if err := refreshUnstagedView(g, entity); err != nil { + return err + } + return nil +} + +func (gui *Gui) addChanges(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + _, files, err := generateFileLists(entity) + if err != nil { + return err + } + if len(files) <= 0 { + return nil + } + _, cy := v.Cursor() + _, oy := v.Origin() + if err := files[cy+oy].Add(git.AddOptions{}); err != nil { + return err + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + return nil +} + +func (gui *Gui) addAllChanges(g *gocui.Gui, v *gocui.View) error { + entity := gui.getSelectedRepository() + if err := entity.AddAll(git.AddOptions{}); err != nil { + return err + } + if err := refreshAllStatusView(g, entity); err != nil { + return err + } + return nil +} + +// refresh the main view and re-render the repository representations +func refreshUnstagedView(g *gocui.Gui, entity *git.RepoEntity) error { + stageView, err := g.View(unstageViewFeature.Name) + if err != nil { + return err + } + stageView.Clear() + _, cy := stageView.Cursor() + _, oy := stageView.Origin() + _, files, err := generateFileLists(entity) + if err != nil { + return err + } + for i, file := range files { + var prefix string + if i == cy+oy { + prefix = prefix + selectionIndicator + } + fmt.Fprintf(stageView, "%s%s%s %s\n", prefix, red.Sprint(string(file.X)), red.Sprint(string(file.Y)), file.Name) + } + return nil +} diff --git a/pkg/helpers/command.go b/pkg/helpers/command.go index 2485b56..b861812 100644 --- a/pkg/helpers/command.go +++ b/pkg/helpers/command.go @@ -19,7 +19,7 @@ func RunCommandWithOutput(dir string, command string, args []string) (string, er } // GetCommandStatus returns if we supposed to get return value as an int of a command -// this method can be used. It is practical when you use a command and process a +// this method can be used. It is practical when you use a command and process a // failover acoording to a soecific return code func GetCommandStatus(dir string, command string, args []string) (int, error) { cmd := exec.Command(command, args...) diff --git a/pkg/queue/job.go b/pkg/queue/job.go index e2dd94e..1fe5ba1 100644 --- a/pkg/queue/job.go +++ b/pkg/queue/job.go @@ -63,6 +63,5 @@ func (job *Job) start() error { return nil } job.Entity.State = git.Success - job.Entity.Refresh() return nil } |
