mirror of
https://github.com/grdl/git-get.git
synced 2026-02-04 18:34:51 +00:00
Refactor packages structure
- Isolate files into their own packages - Create new printer package and interface - Refactor Repo stuct to embed the go-git *Repository directly - Simplify cmd package
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package pkg
|
package cfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package pkg
|
package cfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -156,3 +156,9 @@ func TestConfig(t *testing.T) {
|
|||||||
checkFatal(t, err)
|
checkFatal(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkFatal(t *testing.T, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git-get/pkg"
|
"git-get/cfg"
|
||||||
|
"git-get/git"
|
||||||
|
"git-get/path"
|
||||||
|
"git-get/print"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
pathpkg "path"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -27,30 +32,37 @@ var list bool
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmd.PersistentFlags().BoolVarP(&list, "list", "l", false, "Lists all repositories inside git-get root")
|
cmd.PersistentFlags().BoolVarP(&list, "list", "l", false, "Lists all repositories inside git-get root")
|
||||||
cmd.PersistentFlags().StringP(pkg.KeyReposRoot, "r", "", "repos root")
|
cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "repos root")
|
||||||
cmd.PersistentFlags().StringP(pkg.KeyPrivateKey, "p", "", "SSH private key path")
|
cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "SSH private key path")
|
||||||
viper.BindPFlag(pkg.KeyReposRoot, cmd.PersistentFlags().Lookup(pkg.KeyReposRoot))
|
viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot))
|
||||||
viper.BindPFlag(pkg.KeyPrivateKey, cmd.PersistentFlags().Lookup(pkg.KeyReposRoot))
|
viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(cmd *cobra.Command, args []string) {
|
func Run(cmd *cobra.Command, args []string) {
|
||||||
pkg.InitConfig()
|
cfg.InitConfig()
|
||||||
|
|
||||||
|
root := viper.GetString(cfg.KeyReposRoot)
|
||||||
if list {
|
if list {
|
||||||
paths, err := pkg.FindRepos()
|
paths, err := path.FindRepos()
|
||||||
exitIfError(err)
|
exitIfError(err)
|
||||||
|
|
||||||
repos, err := pkg.OpenAll(paths)
|
repos, err := path.OpenAll(paths)
|
||||||
exitIfError(err)
|
exitIfError(err)
|
||||||
|
|
||||||
pkg.PrintRepos(repos)
|
//tree := BuildTree(root, repos)
|
||||||
|
//fmt.Println(RenderSmartTree(tree))
|
||||||
|
|
||||||
|
printer := print.NewFlatPrinter()
|
||||||
|
fmt.Println(printer.Print(root, repos))
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := pkg.ParseURL(args[0])
|
url, err := path.ParseURL(args[0])
|
||||||
exitIfError(err)
|
exitIfError(err)
|
||||||
|
repoPath := pathpkg.Join(root, path.URLToPath(url))
|
||||||
|
|
||||||
_, err = pkg.CloneRepo(url, viper.GetString(pkg.KeyReposRoot), false)
|
_, err = git.CloneRepo(url, repoPath, false)
|
||||||
exitIfError(err)
|
exitIfError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
package pkg
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git-get/cfg"
|
||||||
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -18,18 +19,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Repo struct {
|
type Repo struct {
|
||||||
repo *git.Repository
|
*git.Repository
|
||||||
path string
|
Path string
|
||||||
Status *RepoStatus
|
Status *RepoStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloneRepo(url *url.URL, reposRoot string, quiet bool) (*Repo, error) {
|
func CloneRepo(url *url.URL, path string, quiet bool) (*Repo, error) {
|
||||||
repoPath := path.Join(reposRoot, URLToPath(url))
|
|
||||||
|
|
||||||
var progress io.Writer
|
var progress io.Writer
|
||||||
if !quiet {
|
if !quiet {
|
||||||
progress = os.Stdout
|
progress = os.Stdout
|
||||||
fmt.Printf("Cloning into '%s'...\n", repoPath)
|
fmt.Printf("Cloning into '%s'...\n", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: can this be cleaner?
|
// TODO: can this be cleaner?
|
||||||
@@ -54,12 +53,12 @@ func CloneRepo(url *url.URL, reposRoot string, quiet bool) (*Repo, error) {
|
|||||||
Tags: git.AllTags,
|
Tags: git.AllTags,
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := git.PlainClone(repoPath, false, opts)
|
repo, err := git.PlainClone(path, false, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed cloning repo")
|
return nil, errors.Wrap(err, "Failed cloning repo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return newRepo(repo, repoPath), nil
|
return NewRepo(repo, path), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenRepo(repoPath string) (*Repo, error) {
|
func OpenRepo(repoPath string) (*Repo, error) {
|
||||||
@@ -68,20 +67,20 @@ func OpenRepo(repoPath string) (*Repo, error) {
|
|||||||
return nil, errors.Wrap(err, "Failed opening repo")
|
return nil, errors.Wrap(err, "Failed opening repo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return newRepo(repo, repoPath), nil
|
return NewRepo(repo, repoPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRepo(repo *git.Repository, repoPath string) *Repo {
|
func NewRepo(repo *git.Repository, repoPath string) *Repo {
|
||||||
return &Repo{
|
return &Repo{
|
||||||
repo: repo,
|
Repository: repo,
|
||||||
path: repoPath,
|
Path: repoPath,
|
||||||
Status: &RepoStatus{},
|
Status: &RepoStatus{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch performs a git fetch on all remotes
|
// Fetch performs a git fetch on all remotes
|
||||||
func (r *Repo) Fetch() error {
|
func (r *Repo) Fetch() error {
|
||||||
remotes, err := r.repo.Remotes()
|
remotes, err := r.Remotes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed getting remotes")
|
return errors.Wrap(err, "Failed getting remotes")
|
||||||
}
|
}
|
||||||
@@ -97,7 +96,7 @@ func (r *Repo) Fetch() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sshKeyAuth() (transport.AuthMethod, error) {
|
func sshKeyAuth() (transport.AuthMethod, error) {
|
||||||
privateKey := viper.GetString(KeyPrivateKey)
|
privateKey := viper.GetString(cfg.KeyPrivateKey)
|
||||||
sshKey, err := ioutil.ReadFile(privateKey)
|
sshKey, err := ioutil.ReadFile(privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "Failed to open ssh private key %s", privateKey)
|
return nil, errors.Wrapf(err, "Failed to open ssh private key %s", privateKey)
|
||||||
@@ -112,3 +111,17 @@ func sshKeyAuth() (transport.AuthMethod, error) {
|
|||||||
auth := &go_git_ssh.PublicKeys{User: "git", Signer: signer}
|
auth := &go_git_ssh.PublicKeys{User: "git", Signer: signer}
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repo) CurrentBranchStatus() *BranchStatus {
|
||||||
|
if r.Status.CurrentBranch == StatusDetached || r.Status.CurrentBranch == StatusUnknown {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, b := range r.Status.Branches {
|
||||||
|
if b.Name == r.Status.CurrentBranch {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package pkg
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -25,7 +27,7 @@ func newRepoEmpty(t *testing.T) *Repo {
|
|||||||
repo, err := git.PlainInit(dir, false)
|
repo, err := git.PlainInit(dir, false)
|
||||||
checkFatal(t, err)
|
checkFatal(t, err)
|
||||||
|
|
||||||
return newRepo(repo, dir)
|
return NewRepo(repo, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRepoWithUntracked(t *testing.T) *Repo {
|
func newRepoWithUntracked(t *testing.T) *Repo {
|
||||||
@@ -156,7 +158,7 @@ func newTempDir(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) writeFile(t *testing.T, name string, content string) {
|
func (r *Repo) writeFile(t *testing.T, name string, content string) {
|
||||||
wt, err := r.repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||||
|
|
||||||
file, err := wt.Filesystem.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
file, err := wt.Filesystem.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
@@ -167,7 +169,7 @@ func (r *Repo) writeFile(t *testing.T, name string, content string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) addFile(t *testing.T, name string) {
|
func (r *Repo) addFile(t *testing.T, name string) {
|
||||||
wt, err := r.repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||||
|
|
||||||
_, err = wt.Add(name)
|
_, err = wt.Add(name)
|
||||||
@@ -175,7 +177,7 @@ func (r *Repo) addFile(t *testing.T, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) newCommit(t *testing.T, msg string) plumbing.Hash {
|
func (r *Repo) newCommit(t *testing.T, msg string) plumbing.Hash {
|
||||||
wt, err := r.repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||||
|
|
||||||
opts := &git.CommitOptions{
|
opts := &git.CommitOptions{
|
||||||
@@ -192,21 +194,21 @@ func (r *Repo) newCommit(t *testing.T, msg string) plumbing.Hash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) newBranch(t *testing.T, name string) {
|
func (r *Repo) newBranch(t *testing.T, name string) {
|
||||||
head, err := r.repo.Head()
|
head, err := r.Head()
|
||||||
checkFatal(t, err)
|
checkFatal(t, err)
|
||||||
|
|
||||||
ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash())
|
ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash())
|
||||||
|
|
||||||
err = r.repo.Storer.SetReference(ref)
|
err = r.Storer.SetReference(ref)
|
||||||
checkFatal(t, err)
|
checkFatal(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) clone(t *testing.T) *Repo {
|
func (r *Repo) clone(t *testing.T) *Repo {
|
||||||
dir := newTempDir(t)
|
dir := newTempDir(t)
|
||||||
url, err := ParseURL("file://" + r.path)
|
repoURL, err := url.Parse("file://" + r.Path)
|
||||||
checkFatal(t, err)
|
checkFatal(t, err)
|
||||||
|
|
||||||
repo, err := CloneRepo(url, dir, true)
|
repo, err := CloneRepo(repoURL, dir, true)
|
||||||
checkFatal(t, err)
|
checkFatal(t, err)
|
||||||
|
|
||||||
return repo
|
return repo
|
||||||
@@ -218,7 +220,7 @@ func (r *Repo) fetch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) checkoutBranch(t *testing.T, name string) {
|
func (r *Repo) checkoutBranch(t *testing.T, name string) {
|
||||||
wt, err := r.repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||||
|
|
||||||
opts := &git.CheckoutOptions{
|
opts := &git.CheckoutOptions{
|
||||||
@@ -229,7 +231,7 @@ func (r *Repo) checkoutBranch(t *testing.T, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) checkoutHash(t *testing.T, hash plumbing.Hash) {
|
func (r *Repo) checkoutHash(t *testing.T, hash plumbing.Hash) {
|
||||||
wt, err := r.repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||||
|
|
||||||
opts := &git.CheckoutOptions{
|
opts := &git.CheckoutOptions{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package pkg
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
@@ -39,7 +39,7 @@ type BranchStatus struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) LoadStatus() error {
|
func (r *Repo) LoadStatus() error {
|
||||||
wt, err := r.repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed getting worktree")
|
return errors.Wrap(err, "Failed getting worktree")
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ func hasUncommitted(status git.Status) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func currentBranch(r *Repo) string {
|
func currentBranch(r *Repo) string {
|
||||||
head, err := r.repo.Head()
|
head, err := r.Head()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StatusUnknown
|
return StatusUnknown
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func currentBranch(r *Repo) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) loadBranchesStatus() error {
|
func (r *Repo) loadBranchesStatus() error {
|
||||||
iter, err := r.repo.Branches()
|
iter, err := r.Branches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed getting branches iterator")
|
return errors.Wrap(err, "Failed getting branches iterator")
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ func (r *Repo) newBranchStatus(branch string) (*BranchStatus, error) {
|
|||||||
// "remote" - name of the remote containing upstream branch (or "." if upstream is a local branch)
|
// "remote" - name of the remote containing upstream branch (or "." if upstream is a local branch)
|
||||||
// "merge" - full ref name of the upstream branch (eg, ref/heads/master)
|
// "merge" - full ref name of the upstream branch (eg, ref/heads/master)
|
||||||
func (r *Repo) upstream(branch string) (string, error) {
|
func (r *Repo) upstream(branch string) (string, error) {
|
||||||
cfg, err := r.repo.Config()
|
cfg, err := r.Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "Failed getting repo config")
|
return "", errors.Wrap(err, "Failed getting repo config")
|
||||||
}
|
}
|
||||||
@@ -205,22 +205,22 @@ func (r *Repo) upstream(branch string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) needsPullOrPush(localBranch string, upstreamBranch string) (needsPull bool, needsPush bool, err error) {
|
func (r *Repo) needsPullOrPush(localBranch string, upstreamBranch string) (needsPull bool, needsPush bool, err error) {
|
||||||
localHash, err := r.repo.ResolveRevision(plumbing.Revision(localBranch))
|
localHash, err := r.ResolveRevision(plumbing.Revision(localBranch))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errors.Wrapf(err, "Failed resolving revision %s", localBranch)
|
return false, false, errors.Wrapf(err, "Failed resolving revision %s", localBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
upstreamHash, err := r.repo.ResolveRevision(plumbing.Revision(upstreamBranch))
|
upstreamHash, err := r.ResolveRevision(plumbing.Revision(upstreamBranch))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errors.Wrapf(err, "Failed resolving revision %s", upstreamBranch)
|
return false, false, errors.Wrapf(err, "Failed resolving revision %s", upstreamBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
localCommit, err := r.repo.CommitObject(*localHash)
|
localCommit, err := r.CommitObject(*localHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", localHash.String())
|
return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", localHash.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
upstreamCommit, err := r.repo.CommitObject(*upstreamHash)
|
upstreamCommit, err := r.CommitObject(*upstreamHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", upstreamHash.String())
|
return false, false, errors.Wrapf(err, "Failed finding a commit for hash %s", upstreamHash.String())
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package pkg
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
103
path/list.go
Normal file
103
path/list.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git-get/cfg"
|
||||||
|
"git-get/git"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/karrick/godirwalk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// skipNode is used as an error indicating that .git directory has been found.
|
||||||
|
// It's handled by ErrorsCallback to tell the WalkCallback to skip this dir.
|
||||||
|
var skipNode = errors.New(".git directory found, skipping this node")
|
||||||
|
|
||||||
|
var repos []string
|
||||||
|
|
||||||
|
func FindRepos() ([]string, error) {
|
||||||
|
repos = []string{}
|
||||||
|
|
||||||
|
root := viper.GetString(cfg.KeyReposRoot)
|
||||||
|
|
||||||
|
if _, err := os.Stat(root); err != nil {
|
||||||
|
return nil, fmt.Errorf("Repos root %s does not exist or can't be accessed", root)
|
||||||
|
}
|
||||||
|
|
||||||
|
walkOpts := &godirwalk.Options{
|
||||||
|
ErrorCallback: ErrorCb,
|
||||||
|
Callback: WalkCb,
|
||||||
|
// Use Unsorted to improve speed because repos will be processed by goroutines in a random order anyway.
|
||||||
|
Unsorted: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := godirwalk.Walk(root, walkOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repos) == 0 {
|
||||||
|
return nil, fmt.Errorf("No git repos found in repos root %s", root)
|
||||||
|
}
|
||||||
|
|
||||||
|
return repos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WalkCb(path string, ent *godirwalk.Dirent) error {
|
||||||
|
if ent.IsDir() && ent.Name() == ".git" {
|
||||||
|
repos = append(repos, strings.TrimSuffix(path, ".git"))
|
||||||
|
return skipNode
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorCb(_ string, err error) godirwalk.ErrorAction {
|
||||||
|
if errors.Is(err, skipNode) {
|
||||||
|
return godirwalk.SkipNode
|
||||||
|
}
|
||||||
|
return godirwalk.Halt
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenAll(paths []string) ([]*git.Repo, error) {
|
||||||
|
var repos []*git.Repo
|
||||||
|
reposChan := make(chan *git.Repo)
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
go func(path string) {
|
||||||
|
repo, err := git.OpenRepo(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// TODO handle error
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repo.LoadStatus()
|
||||||
|
if err != nil {
|
||||||
|
// TODO handle error
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
// when error happened we just sent a nil
|
||||||
|
reposChan <- repo
|
||||||
|
}(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for repo := range reposChan {
|
||||||
|
repos = append(repos, repo)
|
||||||
|
|
||||||
|
// TODO: is this the right way to close the channel? What if we have non-unique paths?
|
||||||
|
if len(repos) == len(paths) {
|
||||||
|
close(reposChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the final array to make printing easier
|
||||||
|
sort.Slice(repos, func(i, j int) bool {
|
||||||
|
return strings.Compare(repos[i].Path, repos[j].Path) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return repos, nil
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package pkg
|
package path
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git-get/cfg"
|
||||||
urlpkg "net/url"
|
urlpkg "net/url"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -46,7 +47,7 @@ func ParseURL(rawURL string) (url *urlpkg.URL, err error) {
|
|||||||
|
|
||||||
// Default to configured defaultHost when host is empty
|
// Default to configured defaultHost when host is empty
|
||||||
if url.Host == "" {
|
if url.Host == "" {
|
||||||
url.Host = viper.GetString(KeyDefaultHost)
|
url.Host = viper.GetString(cfg.KeyDefaultHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to https when scheme is empty
|
// Default to https when scheme is empty
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package pkg
|
package path
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git-get/cfg"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ func TestURLParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We need to init config first so the default values are correctly loaded
|
// We need to init config first so the default values are correctly loaded
|
||||||
InitConfig()
|
cfg.InitConfig()
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
url, err := ParseURL(test.in)
|
url, err := ParseURL(test.in)
|
||||||
198
pkg/list.go
198
pkg/list.go
@@ -1,198 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/karrick/godirwalk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// skipNode is used as an error indicating that .git directory has been found.
|
|
||||||
// It's handled by ErrorsCallback to tell the WalkCallback to skip this dir.
|
|
||||||
var skipNode = errors.New(".git directory found, skipping this node")
|
|
||||||
|
|
||||||
var repos []string
|
|
||||||
|
|
||||||
func FindRepos() ([]string, error) {
|
|
||||||
repos = []string{}
|
|
||||||
|
|
||||||
root := viper.GetString(KeyReposRoot)
|
|
||||||
|
|
||||||
if _, err := os.Stat(root); err != nil {
|
|
||||||
return nil, fmt.Errorf("Repos root %s does not exist or can't be accessed", root)
|
|
||||||
}
|
|
||||||
|
|
||||||
walkOpts := &godirwalk.Options{
|
|
||||||
ErrorCallback: ErrorCb,
|
|
||||||
Callback: WalkCb,
|
|
||||||
// Use Unsorted to improve speed because repos will be processed by goroutines in a random order anyway.
|
|
||||||
Unsorted: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := godirwalk.Walk(root, walkOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(repos) == 0 {
|
|
||||||
return nil, fmt.Errorf("No git repos found in repos root %s", root)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WalkCb(path string, ent *godirwalk.Dirent) error {
|
|
||||||
if ent.IsDir() && ent.Name() == git.GitDirName {
|
|
||||||
repos = append(repos, strings.TrimSuffix(path, git.GitDirName))
|
|
||||||
return skipNode
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorCb(_ string, err error) godirwalk.ErrorAction {
|
|
||||||
if errors.Is(err, skipNode) {
|
|
||||||
return godirwalk.SkipNode
|
|
||||||
}
|
|
||||||
return godirwalk.Halt
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenAll(paths []string) ([]*Repo, error) {
|
|
||||||
var repos []*Repo
|
|
||||||
reposChan := make(chan *Repo)
|
|
||||||
|
|
||||||
for _, path := range paths {
|
|
||||||
go func(path string) {
|
|
||||||
repo, err := OpenRepo(path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// TODO handle error
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = repo.LoadStatus()
|
|
||||||
if err != nil {
|
|
||||||
// TODO handle error
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// when error happened we just sent a nil
|
|
||||||
reposChan <- repo
|
|
||||||
}(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
for repo := range reposChan {
|
|
||||||
repos = append(repos, repo)
|
|
||||||
|
|
||||||
// TODO: is this the right way to close the channel? What if we have non-unique paths?
|
|
||||||
if len(repos) == len(paths) {
|
|
||||||
close(reposChan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the final array to make printing easier
|
|
||||||
sort.Slice(repos, func(i, j int) bool {
|
|
||||||
return strings.Compare(repos[i].path, repos[j].path) < 0
|
|
||||||
})
|
|
||||||
|
|
||||||
return repos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintRepos(repos []*Repo) {
|
|
||||||
root := viper.GetString(KeyReposRoot)
|
|
||||||
|
|
||||||
tree := BuildTree(root, repos)
|
|
||||||
fmt.Println(RenderSmartTree(tree))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ColorRed = "\033[1;31m%s\033[0m"
|
|
||||||
ColorGreen = "\033[1;32m%s\033[0m"
|
|
||||||
ColorBlue = "\033[1;34m%s\033[0m"
|
|
||||||
ColorYellow = "\033[1;33m%s\033[0m"
|
|
||||||
)
|
|
||||||
|
|
||||||
func renderWorktreeStatus(repo *Repo) string {
|
|
||||||
clean := true
|
|
||||||
var status []string
|
|
||||||
|
|
||||||
// if current branch status can't be found it's probably a detached head
|
|
||||||
// TODO: what if current HEAD points to a tag?
|
|
||||||
if current := repo.findCurrentBranchStatus(); current == nil {
|
|
||||||
status = append(status, fmt.Sprintf(ColorYellow, repo.Status.CurrentBranch))
|
|
||||||
} else {
|
|
||||||
status = append(status, renderBranchStatus(current))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is ugly
|
|
||||||
// unset clean flag to use it to render braces around worktree status and remove "ok" from branch status if it's there
|
|
||||||
if repo.Status.HasUncommittedChanges || repo.Status.HasUntrackedFiles {
|
|
||||||
clean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !clean {
|
|
||||||
status[len(status)-1] = strings.TrimSuffix(status[len(status)-1], StatusOk)
|
|
||||||
status = append(status, "[")
|
|
||||||
}
|
|
||||||
|
|
||||||
if repo.Status.HasUntrackedFiles {
|
|
||||||
status = append(status, fmt.Sprintf(ColorRed, StatusUntracked))
|
|
||||||
}
|
|
||||||
|
|
||||||
if repo.Status.HasUncommittedChanges {
|
|
||||||
status = append(status, fmt.Sprintf(ColorRed, StatusUncommitted))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !clean {
|
|
||||||
status = append(status, "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(status, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderBranchStatus(branch *BranchStatus) string {
|
|
||||||
// ok indicates that the branch has upstream and is not ahead or behind it
|
|
||||||
ok := true
|
|
||||||
var status []string
|
|
||||||
|
|
||||||
status = append(status, fmt.Sprintf(ColorBlue, branch.Name))
|
|
||||||
|
|
||||||
if branch.Upstream == "" {
|
|
||||||
ok = false
|
|
||||||
status = append(status, fmt.Sprintf(ColorYellow, StatusNoUpstream))
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch.NeedsPull {
|
|
||||||
ok = false
|
|
||||||
status = append(status, fmt.Sprintf(ColorYellow, StatusBehind))
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch.NeedsPush {
|
|
||||||
ok = false
|
|
||||||
status = append(status, fmt.Sprintf(ColorYellow, StatusAhead))
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
status = append(status, fmt.Sprintf(ColorGreen, StatusOk))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(status, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) findCurrentBranchStatus() *BranchStatus {
|
|
||||||
if r.Status.CurrentBranch == StatusDetached || r.Status.CurrentBranch == StatusUnknown {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, b := range r.Status.Branches {
|
|
||||||
if b.Name == r.Status.CurrentBranch {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
115
print/print.go
Normal file
115
print/print.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package print
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git-get/git"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Printer interface {
|
||||||
|
Print(root string, repos []*git.Repo) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlatPrinter struct{}
|
||||||
|
|
||||||
|
func NewFlatPrinter() *FlatPrinter {
|
||||||
|
return &FlatPrinter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FlatPrinter) Print(root string, repos []*git.Repo) string {
|
||||||
|
val := root
|
||||||
|
|
||||||
|
for _, repo := range repos {
|
||||||
|
path := strings.TrimPrefix(repo.Path, root)
|
||||||
|
path = strings.Trim(path, string(filepath.Separator))
|
||||||
|
|
||||||
|
val += fmt.Sprintf("\n%s %s", path, renderWorktreeStatus(repo))
|
||||||
|
|
||||||
|
for _, branch := range repo.Status.Branches {
|
||||||
|
// Don't print the status of the current branch. It was already printed above.
|
||||||
|
if branch.Name == repo.Status.CurrentBranch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
indent := strings.Repeat(" ", len(path))
|
||||||
|
val += fmt.Sprintf("\n%s %s", indent, renderBranchStatus(branch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorRed = "\033[1;31m%s\033[0m"
|
||||||
|
ColorGreen = "\033[1;32m%s\033[0m"
|
||||||
|
ColorBlue = "\033[1;34m%s\033[0m"
|
||||||
|
ColorYellow = "\033[1;33m%s\033[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
func renderWorktreeStatus(repo *git.Repo) string {
|
||||||
|
clean := true
|
||||||
|
var status []string
|
||||||
|
|
||||||
|
// if current branch status can't be found it's probably a detached head
|
||||||
|
// TODO: what if current HEAD points to a tag?
|
||||||
|
if current := repo.CurrentBranchStatus(); current == nil {
|
||||||
|
status = append(status, fmt.Sprintf(ColorYellow, repo.Status.CurrentBranch))
|
||||||
|
} else {
|
||||||
|
status = append(status, renderBranchStatus(current))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this is ugly
|
||||||
|
// unset clean flag to use it to render braces around worktree status and remove "ok" from branch status if it's there
|
||||||
|
if repo.Status.HasUncommittedChanges || repo.Status.HasUntrackedFiles {
|
||||||
|
clean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !clean {
|
||||||
|
status[len(status)-1] = strings.TrimSuffix(status[len(status)-1], git.StatusOk)
|
||||||
|
status = append(status, "[")
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Status.HasUntrackedFiles {
|
||||||
|
status = append(status, fmt.Sprintf(ColorRed, git.StatusUntracked))
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Status.HasUncommittedChanges {
|
||||||
|
status = append(status, fmt.Sprintf(ColorRed, git.StatusUncommitted))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !clean {
|
||||||
|
status = append(status, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(status, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBranchStatus(branch *git.BranchStatus) string {
|
||||||
|
// ok indicates that the branch has upstream and is not ahead or behind it
|
||||||
|
ok := true
|
||||||
|
var status []string
|
||||||
|
|
||||||
|
status = append(status, fmt.Sprintf(ColorBlue, branch.Name))
|
||||||
|
|
||||||
|
if branch.Upstream == "" {
|
||||||
|
ok = false
|
||||||
|
status = append(status, fmt.Sprintf(ColorYellow, git.StatusNoUpstream))
|
||||||
|
}
|
||||||
|
|
||||||
|
if branch.NeedsPull {
|
||||||
|
ok = false
|
||||||
|
status = append(status, fmt.Sprintf(ColorYellow, git.StatusBehind))
|
||||||
|
}
|
||||||
|
|
||||||
|
if branch.NeedsPush {
|
||||||
|
ok = false
|
||||||
|
status = append(status, fmt.Sprintf(ColorYellow, git.StatusAhead))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
status = append(status, fmt.Sprintf(ColorGreen, git.StatusOk))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(status, " ")
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package pkg
|
package print
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git-get/git"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -11,7 +12,7 @@ type Node struct {
|
|||||||
depth int // depth is a nesting depth used when rendering a tree, not an depth level of a node inside the tree
|
depth int // depth is a nesting depth used when rendering a tree, not an depth level of a node inside the tree
|
||||||
parent *Node
|
parent *Node
|
||||||
children []*Node
|
children []*Node
|
||||||
repo *Repo
|
repo *git.Repo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root creates a new root of a tree
|
// Root creates a new root of a tree
|
||||||
@@ -55,11 +56,11 @@ func (n *Node) GetChild(val string) *Node {
|
|||||||
// BuildTree builds a directory tree of paths to repositories.
|
// BuildTree builds a directory tree of paths to repositories.
|
||||||
// Each node represents a directory in the repo path.
|
// Each node represents a directory in the repo path.
|
||||||
// Each leaf (final node) contains a pointer to the repo.
|
// Each leaf (final node) contains a pointer to the repo.
|
||||||
func BuildTree(root string, repos []*Repo) *Node {
|
func BuildTree(root string, repos []*git.Repo) *Node {
|
||||||
tree := Root(root)
|
tree := Root(root)
|
||||||
|
|
||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
path := strings.TrimPrefix(repo.path, root)
|
path := strings.TrimPrefix(repo.Path, root)
|
||||||
path = strings.Trim(path, string(filepath.Separator))
|
path = strings.Trim(path, string(filepath.Separator))
|
||||||
subs := strings.Split(path, string(filepath.Separator))
|
subs := strings.Split(path, string(filepath.Separator))
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ func RenderSmartTree(node *Node) string {
|
|||||||
|
|
||||||
// TODO: Ugly
|
// TODO: Ugly
|
||||||
// If this is called from tests the repo will be nil and we should return just the name without the status.
|
// If this is called from tests the repo will be nil and we should return just the name without the status.
|
||||||
if node.repo.repo == nil {
|
if node.repo.Repository == nil {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package pkg
|
package print
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git-get/git"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -90,9 +91,9 @@ gitlab.com/
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
var repos []*Repo
|
var repos []*git.Repo
|
||||||
for _, path := range test.paths {
|
for _, path := range test.paths {
|
||||||
repos = append(repos, newRepo(nil, path)) //&Repo{path: path})
|
repos = append(repos, git.NewRepo(nil, path)) //&Repo{path: path})
|
||||||
}
|
}
|
||||||
|
|
||||||
tree := BuildTree("root", repos)
|
tree := BuildTree("root", repos)
|
||||||
Reference in New Issue
Block a user