├── .gitignore ├── LICENSE ├── README.md ├── configurationFile ├── configurationFile.go └── configurationFile_test.go ├── consts └── consts.go ├── gitManip └── gitObject.go ├── main.go ├── pictures └── goyave.png ├── traces └── traces.go ├── utils └── utils.go └── walk └── walk.go /.gitignore: -------------------------------------------------------------------------------- 1 | goyave 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Antonin Carette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goyave 2 | A supervisor for git projects 3 | 4 | _Goyave_ is a simple command-line tool to interact (**read only**) with your local git repositories, just in order to keep an eye on them. 5 | This tool creates and updates a TOML file (in your `$HOME` directory), to speed-up interactions and to perform back-ups if you need. 6 | 7 | ## Visible / Hidden ? 8 | 9 | _Goyave_ allows you to get some informations about _dirty_ git repositories in your system (a _dirty_ repository is a repository that contains non-commited files, modified files, etc...), via the `state` command. 10 | In order to get updates on repositories you are interested in, _Goyave_ uses a binary system: 11 | * repositories you are interested in are considered as **VISIBLE**, 12 | * repositories you want to ignore are considered as **HIDDEN**. 13 | 14 | You can modify the default behaviour of _Goyave_ in your configuration file. 15 | 16 | ## Commands 17 | 18 | * `goyave init` -> Command to create an empty configuration file if this one does not exists on your system 19 | * `goyave add` -> Command to add the current directory in the local configuration file 20 | * `goyave crawl` -> Command to crawl your hard drive to find git repositories - those repositories will be classified as **VISIBLE** or **HIDDEN** according to the local system configuration 21 | * `goyave load` -> Command to load an existing configuration file, to retrieve a previous system (for example, to retrieve a work system after an hard reboot) 22 | * `goyave path` -> Command to get the path of a local git repository (useful if your repositories are spread in your file system) 23 | * `goyave state` -> Command to get the current state of your **VISIBLE** git repositories 24 | 25 | ## The configuration file 26 | 27 | The configuration file is available at `$HOME/.goyave`. 28 | You can find, for example, my goyave configuration file [here](https://github.com/k0pernicus/goyave_conf). 29 | 30 | ## Screenshot 31 | 32 | ![Simple screenshot](./pictures/goyave.png) 33 | 34 | ## How to use it? 35 | 36 | #### If you are using goyave the first time 37 | 38 | 1. `go get github.com/k0pernicus/goyave` 39 | 2. *Optional*: The default behavior of _Goyave_ is set to **VISIBLE** - you can change it before crawling your hard drive 40 | 3. `goyave crawl` (recommended!) 41 | 4. `goyave state` 42 | 43 | #### If you are using goyave using an existing configuration file, on the same machine 44 | 45 | 1. `go get github.com/k0pernicus/goyave` 46 | 2. `mv my_configuration_file ~/.goyave` 47 | 3. `goyave load` 48 | 49 | ## Contributing 50 | 51 | _Goyave_ is my first Go project, and I still nead to learn **a lot** ;-) 52 | If you find bugs, want to improve the code and/or documentation, or add new features, please to create an issue and the associated pull request if you modified anything! :-) 53 | 54 | If you want to create a pull request, this is the procedure to make it great: 55 | 56 | * create an issue to explain the problem you encountered (except for typo), 57 | * fork the project, 58 | * create a local branch to make changes (from the `devel` branch), 59 | * test your changes, 60 | * create a pull request (please compare it with the `devel` branch), 61 | * explain your changes, 62 | * submit ! 63 | 64 | Thank you for your interest in contributing to _Goyave_ ! :-D 65 | 66 | ## Troubleshootings 67 | 68 | * `goyave` supports only `libgit2 v0.27`. In order to get this version, please to make those steps: 69 | * `go get -d github.com/libgit2/git2go # download the code` 70 | * `cd $GOPATH/src/github.com/libgit2/git2go` 71 | * `git submodule update --init # init submodules` 72 | * `make install # install the current version of libgit2 and git2go on your file system` 73 | * `cd $GOPATH/src/github.com/k0pernicus/goyave` 74 | * `go install # install goyave` 75 | 76 | ## LICENSE 77 | 78 | MIT License 79 | -------------------------------------------------------------------------------- /configurationFile/configurationFile.go: -------------------------------------------------------------------------------- 1 | /*Package configurationFile represents Encodable/Decodable Golang structures from/to TOML structures. 2 | * 3 | *The global structure is ConfigurationFile, which is the simple way to store accurtely your local informations. 4 | * 5 | */ 6 | package configurationFile 7 | 8 | import ( 9 | "bytes" 10 | "io/ioutil" 11 | "os" 12 | "os/user" 13 | 14 | "path/filepath" 15 | 16 | "sync" 17 | 18 | "github.com/BurntSushi/toml" 19 | "github.com/k0pernicus/goyave/consts" 20 | "github.com/k0pernicus/goyave/gitManip" 21 | "github.com/k0pernicus/goyave/traces" 22 | "github.com/k0pernicus/goyave/utils" 23 | ) 24 | 25 | /*GetConfigurationFileContent get the content of the local configuration file. 26 | *If no configuration file has been found, create a default one and set the bytes array. 27 | */ 28 | func GetConfigurationFileContent(filePointer *os.File, bytesArray *[]byte) { 29 | fileState, err := filePointer.Stat() 30 | // If the file is empty, get the default structure and save it 31 | if err != nil || fileState.Size() == 0 { 32 | traces.WarningTracer.Println("No (or empty) configuration file - creating default one...") 33 | var fileBuffer bytes.Buffer 34 | cLocalhost := utils.GetHostname() 35 | cUser, err := user.Current() 36 | var cUserName string 37 | if err != nil { 38 | cUserName = consts.DefaultUserName 39 | } else { 40 | cUserName = cUser.Username 41 | } 42 | defaultStructure := Default(cUserName, cLocalhost) 43 | defaultStructure.Encode(&fileBuffer) 44 | *bytesArray = fileBuffer.Bytes() 45 | } else { 46 | b, _ := ioutil.ReadAll(filePointer) 47 | *bytesArray = b 48 | } 49 | } 50 | 51 | /*ConfigurationFile represents the TOML structure of the Goyave configuration file 52 | * 53 | *Properties: 54 | * Author: 55 | * The name of the user 56 | * Local: 57 | * Local informations 58 | * Repositories: 59 | * Local git repositories 60 | * VisibleRepositories: 61 | * A list of local ** visible ** git repositories (** used localy **) 62 | * Groups: 63 | * A list of groups 64 | * locker: 65 | * Mutex to perform concurrent RW on map data structures 66 | */ 67 | type ConfigurationFile struct { 68 | Author string 69 | Local LocalInformations `toml:"local"` 70 | Repositories map[string]GitRepository `toml:"repositories"` 71 | VisibleRepositories VisibleRepositories `toml:"-"` 72 | Groups map[string]Group `toml:"group"` 73 | locker sync.RWMutex `toml:"-"` 74 | } 75 | 76 | /*Default is a constructor for ConfigurationFile 77 | * 78 | *Parameters: 79 | * author: 80 | * The name of the user 81 | * hostname: 82 | * The machine hostname 83 | */ 84 | func Default(author string, hostname string) *ConfigurationFile { 85 | return &ConfigurationFile{ 86 | Author: author, 87 | Local: LocalInformations{ 88 | DefaultTarget: consts.VisibleFlag, 89 | Group: hostname, 90 | }, 91 | Groups: map[string]Group{ 92 | hostname: []string{}, 93 | }, 94 | } 95 | } 96 | 97 | /*AddRepository append the given repository to the list of local repositories, if it does not exists 98 | */ 99 | func (c *ConfigurationFile) AddRepository(path, target string) error { 100 | name := filepath.Base(path) 101 | hostname := utils.GetHostname() 102 | c.locker.Lock() 103 | defer c.locker.Unlock() 104 | robj, ok := c.Repositories[name] 105 | // If the repository exists and the path is ok, stop 106 | if ok && robj.Paths[hostname].Path == path { 107 | return nil 108 | } 109 | // Initialize the new GroupPath structure 110 | cgroup := GroupPath{ 111 | Name: name, 112 | Path: path, 113 | } 114 | // If the repository exists but the path is not ok, update it 115 | if ok { 116 | robj.Paths[hostname] = cgroup 117 | return nil 118 | } 119 | // Otherwise, create a new GitRepository structure, and append it in the Repositories field 120 | c.Repositories[name] = GitRepository{ 121 | Name: name, 122 | Paths: map[string]GroupPath{ 123 | hostname: cgroup, 124 | }, 125 | URL: gitManip.GetRemoteURL(path), 126 | } 127 | // If the user wants to add automatically new repositories as repositories to "follow", change 128 | // his flag as a "visible" repository 129 | if target == consts.VisibleFlag { 130 | c.Groups[hostname] = append(c.Groups[hostname], name) 131 | } 132 | return nil 133 | } 134 | 135 | /*GetPath returns the local path file, for a given repository 136 | */ 137 | func (c *ConfigurationFile) GetPath(repository string) (string, bool) { 138 | gobj, ok := c.VisibleRepositories[repository] 139 | return gobj, ok 140 | } 141 | 142 | /*Process initializes useful fields in the data structure 143 | */ 144 | func (c *ConfigurationFile) Process() { 145 | // If the configuration file is new, initialize the map and finish here 146 | if c.Repositories == nil { 147 | c.Repositories = make(map[string]GitRepository) 148 | } 149 | // Otherwise, initialize useful fields 150 | hostname := utils.GetHostname() 151 | vrepositories, ok := c.Groups[hostname] 152 | if !ok { 153 | traces.InfoTracer.Printf("creating new group '%s'\n", hostname) 154 | c.Groups[hostname] = []string{} 155 | } 156 | c.VisibleRepositories = make(VisibleRepositories) 157 | for _, repository := range vrepositories { 158 | c.VisibleRepositories[repository] = c.Repositories[repository].Paths[hostname].Path 159 | } 160 | } 161 | 162 | /*VisibleRepositories is a map structure to store, for each repository name (and the hostname), the associated path 163 | */ 164 | type VisibleRepositories map[string]string 165 | 166 | /*Method that returns if a repository, identified by his name and his path (optional), exists in the given structure 167 | * If path is empty (empty string), the function will only check the name 168 | */ 169 | func (v VisibleRepositories) exists(name, path string) bool { 170 | _, ok := v[name] 171 | if !ok || path == "" { 172 | return ok 173 | } 174 | return v[name] == path 175 | } 176 | 177 | /*GitRepository represents the structure of a local git repository 178 | * 179 | *Properties: 180 | * Name: 181 | * The custom name of the repository 182 | * Paths: 183 | * Path per group name 184 | * URL: 185 | * The remote URL of the repository (from origin) 186 | */ 187 | type GitRepository struct { 188 | Name string `toml:"name"` 189 | Paths map[string]GroupPath `toml:"paths"` 190 | URL string `toml:"url"` 191 | } 192 | 193 | /*GroupPath represents the structure of a local path, using a given group 194 | * 195 | *Properties: 196 | * Name: 197 | * The name of the local git repository 198 | * Path: 199 | * A string that points to the local git repository 200 | */ 201 | type GroupPath struct { 202 | Name string 203 | Path string 204 | } 205 | 206 | /*Group represents a group of git repositories names 207 | */ 208 | type Group []string 209 | 210 | /*LocalInformations represents your local configuration of Goyave 211 | * 212 | *Properties: 213 | * DefaultEntry: 214 | * The default entry to store a git repository (hidden or visible) 215 | * Group: 216 | * The current group name. 217 | */ 218 | type LocalInformations struct { 219 | DefaultTarget string 220 | Group string 221 | } 222 | 223 | /*DecodeString is a function to decode an entire string (which is the content of a given TOML file) to a ConfigurationFile structure 224 | */ 225 | func DecodeString(c *ConfigurationFile, data string) error { 226 | _, err := toml.Decode(data, *c) 227 | return err 228 | } 229 | 230 | /*DecodeBytesArray is a function to decode an entire string (which is the content of a given TOML file) to a ConfigurationFile structure 231 | */ 232 | func DecodeBytesArray(c *ConfigurationFile, data []byte) error { 233 | _, err := toml.Decode(string(data[:]), *c) 234 | return err 235 | } 236 | 237 | /*Encode is a function to encode a ConfigurationFile structure to a byffer of bytes 238 | */ 239 | func (c *ConfigurationFile) Encode(buffer *bytes.Buffer) error { 240 | return toml.NewEncoder(buffer).Encode(c) 241 | } 242 | -------------------------------------------------------------------------------- /configurationFile/configurationFile_test.go: -------------------------------------------------------------------------------- 1 | package configurationFile 2 | 3 | import "testing" 4 | import "bytes" 5 | import "fmt" 6 | 7 | func TestDecode(t *testing.T) { 8 | const configurationExample = ` 9 | author = 'Antonin' 10 | 11 | [local] 12 | group = 'custom' 13 | 14 | [[visible]] 15 | name = 'visible' 16 | path = '/home/user/visible' 17 | 18 | [[hidden]] 19 | name = 'hidden' 20 | path = '/home/user/hidden' 21 | 22 | [[hidden]] 23 | name = 'hidden2' 24 | path = '/home/user/hidden2' 25 | 26 | [[group]] 27 | name = 'custom' 28 | repositories = ['path'] 29 | ` 30 | var configurationStructure ConfigurationFile 31 | Decode(configurationExample, &configurationStructure) 32 | // Check the Author entry 33 | if configurationStructure.Author != "Antonin" { 34 | t.Errorf("The author in the configuration file example is not 'Antonin' but %s.", configurationStructure.Author) 35 | } 36 | // Check the Local structure 37 | if configurationStructure.Local.Group != "custom" { 38 | t.Errorf("The local group in the configuration file example is not 'custom' but %s.", configurationStructure.Local.Group) 39 | } 40 | // Check the first VisibleRepositories structure 41 | if len(configurationStructure.VisibleRepositories) != 1 { 42 | t.Errorf("The number of visible git repositories is not good, got %d instead of %d.", len(configurationStructure.VisibleRepositories), 1) 43 | } 44 | if configurationStructure.VisibleRepositories[0].Name != "visible" { 45 | t.Errorf("The name of the first visible entry is not correct, got %s instead of %s.", configurationStructure.VisibleRepositories[0].Name, "visible") 46 | } 47 | if configurationStructure.VisibleRepositories[0].Path != "/home/user/visible" { 48 | t.Errorf("The path of the first visible entry is not correct, got %s instead of %s.", configurationStructure.VisibleRepositories[0].Path, "/home/user/visible") 49 | } 50 | // Check the first HiddenRepositories structure 51 | if len(configurationStructure.HiddenRepositories) != 2 { 52 | t.Errorf("The number of hidden git repositories is not good, got %d instead of %d.", len(configurationStructure.HiddenRepositories), 1) 53 | } 54 | if configurationStructure.HiddenRepositories[0].Name != "hidden" { 55 | t.Errorf("The name of the first hidden entry is not correct, got %s instead of %s.", configurationStructure.HiddenRepositories[0].Name, "hidden") 56 | } 57 | if configurationStructure.HiddenRepositories[0].Path != "/home/user/hidden" { 58 | t.Errorf("The path of the first hidden entry is not correct, got %s instead of %s.", configurationStructure.HiddenRepositories[0].Path, "/home/user/hidden") 59 | } 60 | // Check the first Groups structure 61 | if len(configurationStructure.Groups) != 1 { 62 | t.Errorf("The number of groups is not good, got %d instead of %d.", len(configurationStructure.Groups), 1) 63 | } 64 | if configurationStructure.Groups[0].Name != "custom" { 65 | t.Errorf("The name of the first group entry is not correct, got %s instead of %s.", configurationStructure.Groups[0].Name, "custom") 66 | } 67 | if len(configurationStructure.Groups[0].Repositories) != 1 { 68 | t.Errorf("The number of repositories in the first group is not good, got %d instead of %d.", len(configurationStructure.Groups[0].Repositories), 1) 69 | } 70 | } 71 | 72 | func TestEncode(t *testing.T) { 73 | localStructure := ConfigurationFile{ 74 | Author: "Antonin", 75 | Local: LocalInformations{ 76 | Group: "local", 77 | }, 78 | VisibleRepositories: []GitRepository{ 79 | GitRepository{ 80 | Name: "visible_example", 81 | Path: "/home/user/mypath", 82 | }, 83 | }, 84 | } 85 | buffer := new(bytes.Buffer) 86 | Encode(&localStructure, buffer) 87 | fmt.Println(buffer) 88 | } 89 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | /*Package consts implements constants for the entire project 2 | */ 3 | package consts 4 | 5 | // DefaultUserName is a constant to define a new user, if the 6 | // user local name can't be found 7 | const DefaultUserName = "Thor" 8 | 9 | // VisibleFlag is the constant given for a visible repository 10 | const VisibleFlag = "VISIBLE" 11 | 12 | // HiddenFlag is the constant given for an hidden repository 13 | const HiddenFlag = "HIDDEN" 14 | 15 | // ConfigurationFileName is the configuration file name of Goyave 16 | const ConfigurationFileName = ".goyave" 17 | 18 | // GitFileName is the name of the git directory, in a git repository 19 | const GitFileName = ".git" 20 | -------------------------------------------------------------------------------- /gitManip/gitObject.go: -------------------------------------------------------------------------------- 1 | package gitManip 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | 8 | "bytes" 9 | 10 | "github.com/k0pernicus/goyave/traces" 11 | git "gopkg.in/libgit2/git2go.v27" 12 | ) 13 | 14 | /*Map to match the RepositoryState enum type with a string 15 | */ 16 | var repositoryStateToString = map[git.RepositoryState]string{ 17 | git.RepositoryStateNone: "None", 18 | git.RepositoryStateMerge: "Merge", 19 | git.RepositoryStateRevert: "Revert", 20 | git.RepositoryStateCherrypick: "Cherrypick", 21 | git.RepositoryStateBisect: "Bisect", 22 | git.RepositoryStateRebase: "Rebase", 23 | git.RepositoryStateRebaseInteractive: "Rebase Interactive", 24 | git.RepositoryStateRebaseMerge: "Rebase Merge", 25 | git.RepositoryStateApplyMailbox: "Apply Mailbox", 26 | git.RepositoryStateApplyMailboxOrRebase: "Apply Mailbox or Rebase", 27 | } 28 | 29 | /*Global variable to set the StatusOption parameter, in order to list each file status 30 | */ 31 | var statusOption = git.StatusOptions{ 32 | Show: git.StatusShowIndexAndWorkdir, 33 | Flags: git.StatusOptIncludeUntracked, 34 | Pathspec: []string{}, 35 | } 36 | 37 | /*GitObject contains informations about the current git repository 38 | * 39 | *The structure is: 40 | * accessible: 41 | * Is the repository still exists in the hard drive? 42 | * path: 43 | * The path file. 44 | * repository: 45 | * The object repository. 46 | */ 47 | type GitObject struct { 48 | accessible error 49 | path string 50 | repository git.Repository 51 | } 52 | 53 | /*New is a constructor for GitObject 54 | * 55 | * It neeeds: 56 | * path: 57 | * The path of the current repository. 58 | */ 59 | func New(path string) *GitObject { 60 | r, err := git.OpenRepository(path) 61 | return &GitObject{accessible: err, path: path, repository: *r} 62 | } 63 | 64 | /*Clone is cloning a given repository, from a public URL 65 | * 66 | * It needs: 67 | * path: 68 | * The local path to clone the repository. 69 | * URL: 70 | * The remote URL to fetch the repository. 71 | */ 72 | func Clone(path, URL string) error { 73 | _, err := git.Clone(URL, path, &git.CloneOptions{}) 74 | return err 75 | } 76 | 77 | /*GetRemoteURL returns the associated remote URL of a given local path repository 78 | * 79 | * It needs: 80 | * path 81 | * The local path of a git repository 82 | */ 83 | func GetRemoteURL(path string) string { 84 | r, err := git.OpenRepository(path) 85 | if err != nil { 86 | fmt.Println("The repository can't be opened") 87 | return "" 88 | } 89 | remoteCollection := r.Remotes 90 | originRemote, err := remoteCollection.Lookup("origin") 91 | if err != nil { 92 | traces.WarningTracer.Printf("can't lookup origin remote URL for %s", path) 93 | return "" 94 | } 95 | return originRemote.Url() 96 | } 97 | 98 | /*isAccesible returns the information that is the current git repository is existing or not. 99 | *This method returns a boolean value: true if the git repository is still accesible (still exists), or false if not. 100 | */ 101 | func (g *GitObject) isAccessible() bool { 102 | return g.accessible == nil 103 | } 104 | 105 | /*Status prints the current status of the repository, accessible via the structure path field. 106 | *This method works only if the repository is accessible. 107 | */ 108 | func (g *GitObject) Status() { 109 | if g.isAccessible() { 110 | if err := g.printChanges(); err != nil { 111 | color.RedString("Impossible to get stats from %s, due to error %s", g.path, err) 112 | } 113 | } else { 114 | color.RedString("Repository %s not found!", g.path) 115 | } 116 | } 117 | 118 | /*getDiffWithWT returns the difference between the working tree and the index, for the current git repository. 119 | *If there is an error processing the request, it returns an error. 120 | */ 121 | func (g *GitObject) getDiffWithWT() (*git.Diff, error) { 122 | // Get the index of the repository 123 | currentIndex, err := g.repository.Index() 124 | if err != nil { 125 | return nil, err 126 | } 127 | // Get the default diff options, and add it custom flags 128 | defaultDiffOptions, err := git.DefaultDiffOptions() 129 | if err != nil { 130 | return nil, err 131 | } 132 | defaultDiffOptions.Flags = defaultDiffOptions.Flags | git.DiffNormal | git.DiffIncludeUntracked | git.DiffIncludeTypeChange 133 | // Check the difference between the working directory and the index 134 | diff, err := g.repository.DiffIndexToWorkdir(currentIndex, &defaultDiffOptions) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return diff, nil 139 | } 140 | 141 | func (g *GitObject) getCommitsAheadBehind() (int, int, error) { 142 | repositoryHead, err := g.repository.Head() 143 | // Check upstream branch head 144 | cBranch := repositoryHead.Branch() 145 | cReference, err := cBranch.Upstream() 146 | if err != nil { 147 | return -1, -1, err 148 | } 149 | cReferenceTarget := cReference.Target() 150 | cRepositoryTarget := repositoryHead.Target() 151 | commitsAhead, commitsBehind, err := g.repository.AheadBehind(cRepositoryTarget, cReferenceTarget) 152 | return commitsAhead, commitsBehind, err 153 | } 154 | 155 | /*printChanges prints out all changes for the current git repository. 156 | *If there is an error processing the request, it returns this one. 157 | */ 158 | func (g *GitObject) printChanges() error { 159 | diff, err := g.getDiffWithWT() 160 | var buffer bytes.Buffer 161 | if err != nil { 162 | return err 163 | } 164 | 165 | numDeltas, err := diff.NumDeltas() 166 | if err != nil { 167 | return err 168 | } 169 | 170 | headDetached, err := g.repository.IsHeadDetached() 171 | if err != nil { 172 | return err 173 | } 174 | if headDetached { 175 | outputHead := fmt.Sprintf("%s", color.RedString("\t/!\\ The repository's HEAD is detached! /!\\\n")) 176 | buffer.WriteString(outputHead) 177 | } 178 | 179 | if numDeltas > 0 { 180 | buffer.WriteString(fmt.Sprintf("%s %9s\t[%d modification(s)]\n", color.RedString("✘"), g.path, numDeltas)) 181 | for i := 0; i < numDeltas; i++ { 182 | delta, _ := diff.GetDelta(i) 183 | currentStatus := delta.Status 184 | newFile := delta.NewFile.Path 185 | oldFile := delta.OldFile.Path 186 | switch currentStatus { 187 | case git.DeltaAdded: 188 | buffer.WriteString(fmt.Sprintf("\t===> %s has been added!\n", color.MagentaString(newFile))) 189 | case git.DeltaDeleted: 190 | buffer.WriteString(fmt.Sprintf("\t===> %s has been deleted!\n", color.MagentaString(newFile))) 191 | case git.DeltaModified: 192 | buffer.WriteString(fmt.Sprintf("\t===> %s has been modified!\n", color.MagentaString(newFile))) 193 | case git.DeltaRenamed: 194 | buffer.WriteString(fmt.Sprintf("\t===> %s has been renamed to %s!\n", color.MagentaString(oldFile), color.MagentaString(newFile))) 195 | case git.DeltaUntracked: 196 | buffer.WriteString(fmt.Sprintf("\t===> %s is untracked - please to add it or update the gitignore file!\n", color.MagentaString(newFile))) 197 | case git.DeltaTypeChange: 198 | buffer.WriteString(fmt.Sprintf("\t===> the type of %s has been changed from %d to %d!", color.MagentaString(newFile), delta.OldFile.Mode, delta.NewFile.Mode)) 199 | } 200 | } 201 | } else { 202 | buffer.WriteString(fmt.Sprintf("%s %s\n", color.GreenString("✔"), g.path)) 203 | } 204 | 205 | commitsAhead, commitsBehind, err := g.getCommitsAheadBehind() 206 | if err == nil { 207 | if commitsAhead != 0 { 208 | buffer.WriteString(fmt.Sprintf("\t%s %d commits AHEAD - Soon, you will need to push your modifications\n", color.RedString("⟳"), commitsAhead)) 209 | } 210 | if commitsBehind != 0 { 211 | buffer.WriteString(fmt.Sprintf("\t%s %d commits BEHIND - Soon, you will need to pull the modifications from the remote branch\n", color.RedString("⟲"), commitsBehind)) 212 | } 213 | } 214 | 215 | fmt.Print(buffer.String()) 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | "sync" 12 | 13 | "github.com/BurntSushi/toml" 14 | "github.com/k0pernicus/goyave/configurationFile" 15 | "github.com/k0pernicus/goyave/consts" 16 | "github.com/k0pernicus/goyave/gitManip" 17 | "github.com/k0pernicus/goyave/traces" 18 | "github.com/k0pernicus/goyave/utils" 19 | "github.com/k0pernicus/goyave/walk" 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | var configurationFileStructure configurationFile.ConfigurationFile 24 | var configurationFilePath string 25 | var userHomeDir string 26 | 27 | /*initialize get the configuration file existing in the system (or create it), and return 28 | *a pointer to his content. 29 | */ 30 | func initialize(configurationFileStructure *configurationFile.ConfigurationFile) { 31 | // Initialize all different traces structures 32 | traces.InitTraces(os.Stdout, os.Stderr, os.Stdout, os.Stdout) 33 | // Get the user home directory 34 | userHomeDir = utils.GetUserHomeDir() 35 | if len(userHomeDir) == 0 { 36 | log.Fatalf("can't get the user home dir\n") 37 | } 38 | // Set the configuration path file 39 | configurationFilePath = path.Join(userHomeDir, consts.ConfigurationFileName) 40 | filePointer, err := os.OpenFile(configurationFilePath, os.O_RDWR|os.O_CREATE, 0755) 41 | if err != nil { 42 | log.Fatalf("can't open the file %s, due to error '%s'\n", configurationFilePath, err) 43 | } 44 | defer filePointer.Close() 45 | var bytesArray []byte 46 | // Get the content of the goyave configuration file 47 | configurationFile.GetConfigurationFileContent(filePointer, &bytesArray) 48 | if _, err = toml.Decode(string(bytesArray[:]), configurationFileStructure); err != nil { 49 | log.Fatalln(err) 50 | } 51 | configurationFileStructure.Process() 52 | } 53 | 54 | /*kill saves the current state of the configuration structure in the configuration file 55 | */ 56 | func kill() { 57 | var outputBuffer bytes.Buffer 58 | if err := configurationFileStructure.Encode(&outputBuffer); err != nil { 59 | log.Fatalln("can't save the current configurationFile structure") 60 | } 61 | if err := ioutil.WriteFile(configurationFilePath, outputBuffer.Bytes(), 0777); err != nil { 62 | log.Fatalln("can't access to your file to save the configurationFile structure") 63 | } 64 | } 65 | 66 | func main() { 67 | 68 | /*rootCmd defines the global app, and some actions to run before and after the command running 69 | */ 70 | var rootCmd = &cobra.Command{ 71 | Use: "goyave", 72 | Short: "Goyave is a tool to take a look at your local git repositories", 73 | // Initialize the structure 74 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 75 | initialize(&configurationFileStructure) 76 | }, 77 | // Save the current configuration file structure, in the configuration file 78 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 79 | kill() 80 | }, 81 | } 82 | 83 | /*addCmd is a subcommand to add the current working directory as a VISIBLE one 84 | */ 85 | var addCmd = &cobra.Command{ 86 | Use: "add", 87 | Short: "Add the current path as a VISIBLE repository", 88 | Run: func(cmd *cobra.Command, args []string) { 89 | // Get the path where the command has been executed 90 | currentDir, err := os.Getwd() 91 | if err != nil { 92 | log.Fatalln("There was a problem retrieving the current directory") 93 | } 94 | if utils.IsGitRepository(currentDir) { 95 | log.Fatalf("%s is not a git repository!\n", currentDir) 96 | } 97 | // If the path is/contains a .git directory, add this one as a VISIBLE repository 98 | if err := configurationFileStructure.AddRepository(currentDir, consts.VisibleFlag); err != nil { 99 | traces.WarningTracer.Printf("[%s] %s\n", currentDir, err) 100 | } 101 | }, 102 | } 103 | 104 | /*crawlCmd is a subcommand to crawl your hard drive in order to get and save new git repositories 105 | */ 106 | var crawlCmd = &cobra.Command{ 107 | Use: "crawl", 108 | Short: "Crawl the hard drive in order to find git repositories", 109 | Run: func(cmd *cobra.Command, args []string) { 110 | var wg sync.WaitGroup 111 | // Get all git paths, and display them 112 | gitPaths, err := walk.RetrieveGitRepositories(userHomeDir) 113 | if err != nil { 114 | log.Fatalf("there was an error retrieving your git repositories: '%s'\n", err) 115 | } 116 | // For each git repository, check if it exists, and if not add it to the default target visibility 117 | for _, gitPath := range gitPaths { 118 | wg.Add(1) 119 | go func(gitPath string) { 120 | defer wg.Done() 121 | if utils.IsGitRepository(gitPath) { 122 | configurationFileStructure.AddRepository(gitPath, configurationFileStructure.Local.DefaultTarget) 123 | } 124 | }(gitPath) 125 | } 126 | wg.Wait() 127 | }, 128 | } 129 | 130 | /*loadCmd permits to load visible repositories from the goyave configuration file 131 | */ 132 | var loadCmd = &cobra.Command{ 133 | Use: "load", 134 | Short: "Load the configuration file to restore your previous work space", 135 | Run: func(cmd *cobra.Command, args []string) { 136 | currentHostname := utils.GetHostname() 137 | traces.InfoTracer.Printf("Current hostname is %s\n", currentHostname) 138 | for { 139 | _, ok := configurationFileStructure.Groups[currentHostname] 140 | if !ok { 141 | traces.WarningTracer.Printf("Your current local host (%s) has not been found!", currentHostname) 142 | fmt.Println("Please to choose one of those, to load the configuration file:") 143 | for group := range configurationFileStructure.Groups { 144 | traces.WarningTracer.Printf("\t%s\n", group) 145 | } 146 | scanner := bufio.NewScanner(os.Stdin) 147 | currentHostname = scanner.Text() 148 | continue 149 | } else { 150 | traces.InfoTracer.Println("Hostname found!") 151 | } 152 | break 153 | } 154 | var wg sync.WaitGroup 155 | for _, repository := range configurationFileStructure.Repositories { 156 | wg.Add(1) 157 | go func(repository configurationFile.GitRepository) { 158 | defer wg.Done() 159 | cName := repository.Name 160 | cPath := repository.Paths[currentHostname].Path 161 | cURL := repository.URL 162 | if _, err := os.Stat(cPath); err == nil { 163 | traces.InfoTracer.Printf("the repository \"%s\" already exists as a local git repository\n", cName) 164 | return 165 | } 166 | traces.InfoTracer.Printf("importing %s...\n", cName) 167 | if err := gitManip.Clone(cPath, cURL); err != nil { 168 | traces.ErrorTracer.Printf("the repository \"%s\" can't be cloned: %s\n", cName, err) 169 | } 170 | }(repository) 171 | } 172 | wg.Wait() 173 | }, 174 | } 175 | 176 | /*pathCmd is a subcommand to get the path of a given git repository. 177 | *This subcommand is useful to change directory, like `cd $(goyave path mygitrepo)` 178 | */ 179 | var pathCmd = &cobra.Command{ 180 | Use: "path", 181 | Short: "Get the path of a given repository, if this one exists", 182 | Run: func(cmd *cobra.Command, args []string) { 183 | if len(args) == 0 { 184 | log.Fatalln("Needs a repository name!") 185 | } 186 | repo := args[0] 187 | repoPath, found := configurationFileStructure.GetPath(repo) 188 | if !found { 189 | log.Fatalf("repository %s not found\n", repo) 190 | } else { 191 | fmt.Println(repoPath) 192 | } 193 | }, 194 | } 195 | 196 | /*stateCmd is a subcommand to list the state of each local git repository. 197 | */ 198 | var stateCmd = &cobra.Command{ 199 | Use: "state", 200 | Example: "goyave state\ngoyave state myRepositoryName\ngoyave state myRepositoryName1 myRepositoryName2", 201 | Short: "Get the state of each local visible git repository", 202 | Long: "Check only visible git repositories.\nIf some repository names have been setted, goyave will only check those repositories, otherwise it checks all visible repositories of your system.", 203 | Run: func(cmd *cobra.Command, args []string) { 204 | var paths []string 205 | // Append repositories to check 206 | if len(args) == 0 { 207 | for _, p := range configurationFileStructure.VisibleRepositories { 208 | paths = append(paths, p) 209 | } 210 | } else { 211 | for _, repository := range args { 212 | repoPath, ok := configurationFileStructure.VisibleRepositories[repository] 213 | if ok { 214 | paths = append(paths, repoPath) 215 | } else { 216 | traces.WarningTracer.Printf("%s cannot be found in your visible repositories\n", repository) 217 | } 218 | } 219 | } 220 | var wg sync.WaitGroup 221 | for _, repository := range paths { 222 | wg.Add(1) 223 | go func(repoPath string) { 224 | defer wg.Done() 225 | cGitObj := gitManip.New(repoPath) 226 | cGitObj.Status() 227 | }(repository) 228 | } 229 | wg.Wait() 230 | }, 231 | } 232 | 233 | rootCmd.AddCommand(addCmd, crawlCmd, loadCmd, pathCmd, stateCmd) 234 | 235 | if err := rootCmd.Execute(); err != nil { 236 | fmt.Println(err) 237 | os.Exit(1) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /pictures/goyave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0pernicus/goyave/6dd38b045375da1910820c91fe732e8b91616828/pictures/goyave.png -------------------------------------------------------------------------------- /traces/traces.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | /*DebugTracer is a Logger object to output logs to debug the program 11 | *ErrorTracer is a Logger object to output logs for runtime errors 12 | *InfoTracer is a Logger object to output basic informations about the program 13 | *WarningTracer is a Logger object to output runtime warnings - warnings are informative messages that are not considered as errors 14 | */ 15 | var ( 16 | DebugTracer *log.Logger 17 | ErrorTracer *log.Logger 18 | InfoTracer *log.Logger 19 | WarningTracer *log.Logger 20 | ) 21 | 22 | /*InitTraces is a function that initialize Loggers 23 | */ 24 | func InitTraces(debugHandle io.Writer, errorHandle io.Writer, infoHandle io.Writer, warningHandle io.Writer) { 25 | 26 | /*Initialize the debug field 27 | */ 28 | DebugTracer = log.New(debugHandle, color.BlueString("DEBUG: "), log.Ldate|log.Ltime|log.Lshortfile) 29 | 30 | /*Initialize the error field 31 | */ 32 | ErrorTracer = log.New(errorHandle, color.RedString("ERROR: "), log.Ldate|log.Ltime|log.Lshortfile) 33 | 34 | /*Initialize the info field 35 | */ 36 | InfoTracer = log.New(infoHandle, color.CyanString("INFO: "), log.Ldate|log.Ltime|log.Lshortfile) 37 | 38 | /*Initialize the warning field 39 | */ 40 | WarningTracer = log.New(warningHandle, color.YellowString("WARNING: "), log.Ldate|log.Ltime|log.Lshortfile) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path/filepath" 7 | 8 | "github.com/k0pernicus/goyave/consts" 9 | ) 10 | 11 | /*IsGitRepository returns if the path, given as an argument, is a git repository or not. 12 | *This function returns a boolean value: true if the pathdir pointed to a git repository, else false. 13 | */ 14 | func IsGitRepository(pathdir string) bool { 15 | if filepath.Base(pathdir) != consts.GitFileName { 16 | pathdir = filepath.Join(pathdir, consts.GitFileName) 17 | } 18 | file, err := os.Open(pathdir) 19 | if err != nil { 20 | return false 21 | } 22 | _, err = file.Stat() 23 | return !os.IsNotExist(err) 24 | } 25 | 26 | /*GetUserHomeDir returns the home directory of the current user. 27 | */ 28 | func GetUserHomeDir() string { 29 | usr, err := user.Current() 30 | // If the current user cannot be reached, get the HOME environment variable 31 | if err != nil { 32 | return os.Getenv("$HOME") 33 | } 34 | return usr.HomeDir 35 | } 36 | 37 | /*GetHostname returns the hostname name of the current computer. 38 | *If there is an error, it returns a default string. 39 | */ 40 | func GetHostname() string { 41 | lhost, err := os.Hostname() 42 | if err != nil { 43 | return "DefaultHostname" 44 | } 45 | return lhost 46 | } 47 | 48 | /*SliceIndex returns the index of the element searched. 49 | *If the element is not in the slice, the function returns -1. 50 | */ 51 | func SliceIndex(limit int, predicate func(i int) bool) int { 52 | for i := 0; i < limit; i++ { 53 | if predicate(i) { 54 | return i 55 | } 56 | } 57 | return -1 58 | } 59 | -------------------------------------------------------------------------------- /walk/walk.go: -------------------------------------------------------------------------------- 1 | package walk 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/k0pernicus/goyave/consts" 8 | "github.com/k0pernicus/goyave/traces" 9 | ) 10 | 11 | /*RetrieveGitRepositories returns an array of strings, which represent paths to git repositories. 12 | *Also, this function returns an error type, that is corresponding to the Walk function behaviour (ok or not). 13 | */ 14 | func RetrieveGitRepositories(rootpath string) ([]string, error) { 15 | var gitPaths []string 16 | err := filepath.Walk(rootpath, func(pathdir string, fileInfo os.FileInfo, err error) error { 17 | if fileInfo.IsDir() && filepath.Base(pathdir) == consts.GitFileName { 18 | fileDir := filepath.Dir(pathdir) 19 | traces.DebugTracer.Printf("Just found in hard drive %s\n", fileDir) 20 | gitPaths = append(gitPaths, fileDir) 21 | } 22 | return nil 23 | }) 24 | return gitPaths, err 25 | } 26 | --------------------------------------------------------------------------------