diff --git a/cfg/config.go b/cfg/config.go index 3bfc0c2..7ae2c04 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -18,6 +18,14 @@ const ( DefDefaultHost = "github.com" KeyPrivateKey = "privateKey" DefPrivateKey = "id_rsa" + KeyOutput = "out" + DefOutput = OutFlat +) + +const ( + OutFlat = "flat" + OutSmart = "smart" + OutSimple = "simple" ) // gitconfig provides methods for looking up configiration values inside .gitconfig file diff --git a/cmd/main.go b/cmd/main.go index 90fa88e..54e1cae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,7 +24,7 @@ var cmd = &cobra.Command{ Use: "git-get ", Short: "git get", Run: Run, - Args: cobra.MaximumNArgs(1), + Args: cobra.MaximumNArgs(1), // TODO: add custom validator Version: fmt.Sprintf("%s - %s, build at %s", version, commit, date), } @@ -34,8 +34,10 @@ func init() { cmd.PersistentFlags().BoolVarP(&list, "list", "l", false, "Lists all repositories inside git-get root") cmd.PersistentFlags().StringP(cfg.KeyReposRoot, "r", "", "repos root") cmd.PersistentFlags().StringP(cfg.KeyPrivateKey, "p", "", "SSH private key path") + cmd.PersistentFlags().StringP(cfg.KeyOutput, "o", cfg.DefOutput, "output format.") viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) viper.BindPFlag(cfg.KeyPrivateKey, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + viper.BindPFlag(cfg.KeyOutput, cmd.PersistentFlags().Lookup(cfg.KeyOutput)) } func Run(cmd *cobra.Command, args []string) { @@ -43,16 +45,26 @@ func Run(cmd *cobra.Command, args []string) { root := viper.GetString(cfg.KeyReposRoot) if list { + // TODO: move it to OpenAll and don't export paths, err := path.FindRepos() exitIfError(err) repos, err := path.OpenAll(paths) exitIfError(err) - //tree := BuildTree(root, repos) - //fmt.Println(RenderSmartTree(tree)) + var printer print.Printer + switch viper.GetString(cfg.KeyOutput) { + case cfg.OutFlat: + printer = &print.FlatPrinter{} + case cfg.OutSimple: + printer = &print.SimpleTreePrinter{} + case cfg.OutSmart: + printer = &print.SmartTreePrinter{} + default: + err = fmt.Errorf("invalid --output flag; allowed values: %v", []string{cfg.OutFlat, cfg.OutSimple, cfg.OutSmart}) + } + exitIfError(err) - printer := print.NewFlatPrinter() fmt.Println(printer.Print(root, repos)) os.Exit(0) diff --git a/go.mod b/go.mod index aca34ff..28c1896 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 + github.com/xlab/treeprint v1.0.0 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 ) diff --git a/go.sum b/go.sum index 02a2a85..b88f76e 100644 --- a/go.sum +++ b/go.sum @@ -230,6 +230,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.0.0 h1:J0TkWtiuYgtdlrkkrDLISYBQ92M+X5m4LrIIMKrbDTs= +github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/print/flat.go b/print/flat.go new file mode 100644 index 0000000..8690d74 --- /dev/null +++ b/print/flat.go @@ -0,0 +1,33 @@ +package print + +import ( + "fmt" + "git-get/git" + "path/filepath" + "strings" +) + +type FlatPrinter struct{} + +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, printWorktreeStatus(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, printBranchStatus(branch)) + } + } + + return val +} diff --git a/print/print.go b/print/print.go index e23b2fc..8769e78 100644 --- a/print/print.go +++ b/print/print.go @@ -3,7 +3,6 @@ package print import ( "fmt" "git-get/git" - "path/filepath" "strings" ) @@ -11,35 +10,6 @@ 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" @@ -47,7 +17,7 @@ const ( ColorYellow = "\033[1;33m%s\033[0m" ) -func renderWorktreeStatus(repo *git.Repo) string { +func printWorktreeStatus(repo *git.Repo) string { clean := true var status []string @@ -56,7 +26,7 @@ func renderWorktreeStatus(repo *git.Repo) string { if current := repo.CurrentBranchStatus(); current == nil { status = append(status, fmt.Sprintf(ColorYellow, repo.Status.CurrentBranch)) } else { - status = append(status, renderBranchStatus(current)) + status = append(status, printBranchStatus(current)) } // TODO: this is ugly @@ -85,7 +55,7 @@ func renderWorktreeStatus(repo *git.Repo) string { return strings.Join(status, " ") } -func renderBranchStatus(branch *git.BranchStatus) string { +func printBranchStatus(branch *git.BranchStatus) string { // ok indicates that the branch has upstream and is not ahead or behind it ok := true var status []string diff --git a/print/tree.go b/print/tree.go index 8fd884e..72ee171 100644 --- a/print/tree.go +++ b/print/tree.go @@ -4,12 +4,39 @@ import ( "git-get/git" "path/filepath" "strings" + + "github.com/xlab/treeprint" ) -// Node represents a node in a repos tree +type SimpleTreePrinter struct{} + +type SmartTreePrinter struct { + // length is the size (number of chars) of the currently processed line. + // It's used to correctly indent the lines with branches status. + length int +} + +func (p *SmartTreePrinter) Print(root string, repos []*git.Repo) string { + tree := BuildTree(root, repos) + + return p.PrintSmartTree(tree) +} + +func (p *SimpleTreePrinter) Print(root string, repos []*git.Repo) string { + tree := BuildTree(root, repos) + + tp := treeprint.New() + tp.SetValue(root) + + p.PrintSimpleTree(tree, tp) + + return tp.String() +} + +// Node represents a node (ie. path fragment) in a repos tree. type Node struct { val string - 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 smart tree, not a depth level of a tree node. parent *Node children []*Node repo *git.Repo @@ -87,7 +114,7 @@ func BuildTree(root string, repos []*git.Repo) *Node { return tree } -// RenderSmartTree returns a string representation of repos tree. +// PrintSmartTree returns a string representation of repos tree. // It's "smart" because it automatically folds branches which only have a single child and indents branches with many children. // // It recursively traverses the tree and prints its nodes. @@ -109,7 +136,7 @@ func BuildTree(root string, repos []*git.Repo) *Node { // repo2 // another/repo // -func RenderSmartTree(node *Node) string { +func (p *SmartTreePrinter) PrintSmartTree(node *Node) string { if node.children == nil { // If node is a leaf, print repo name and its status and finish processing this node. value := node.val @@ -120,17 +147,17 @@ func RenderSmartTree(node *Node) string { return value } - value += " " + renderWorktreeStatus(node.repo) + value += " " + printWorktreeStatus(node.repo) // Print the status of each branch on a new line, indented to match the position of the current branch name. - indent := "\n" + strings.Repeat(" ", length+len(node.val)) + indent := "\n" + strings.Repeat(" ", p.length+len(node.val)) for _, branch := range node.repo.Status.Branches { // Don't print the status of the current branch. It was already printed above. if branch.Name == node.repo.Status.CurrentBranch { continue } - value += indent + renderBranchStatus(branch) + value += indent + printBranchStatus(branch) } return value @@ -148,24 +175,40 @@ func RenderSmartTree(node *Node) string { // node's path has multiple levels above. node.depth = node.parent.depth - length += len(val) + p.length += len(val) } else { // If node has multiple children, print each of them on a new line // and indent them once relative to the parent node.depth = node.parent.depth + 1 shift = "\n" + strings.Repeat(" ", node.depth) - length = 0 + p.length = 0 } for _, child := range node.children { - length += len(shift) - val += shift + RenderSmartTree(child) - length = 0 + p.length += len(shift) + val += shift + p.PrintSmartTree(child) + p.length = 0 } return val } -// lenght is the size (number of chars) of the currently processed line. -// It's used to correctly indent the lines with branches status. -var length int +func (p *SimpleTreePrinter) PrintSimpleTree(node *Node, tp treeprint.Tree) { + if node.children == nil { + tp.SetValue(node.val + " " + printWorktreeStatus(node.repo)) + + for _, branch := range node.repo.Status.Branches { + // Don't print the status of the current branch. It was already printed above. + if branch.Name == node.repo.Status.CurrentBranch { + continue + } + tp.AddNode(printBranchStatus(branch)) + } + + } + + for _, child := range node.children { + branch := tp.AddBranch(child.val) + p.PrintSimpleTree(child, branch) + } +} diff --git a/print/tree_test.go b/print/tree_test.go index 0ad4dcd..1f3e956 100644 --- a/print/tree_test.go +++ b/print/tree_test.go @@ -96,9 +96,9 @@ gitlab.com/ repos = append(repos, git.NewRepo(nil, path)) //&Repo{path: path}) } - tree := BuildTree("root", repos) + printer := SmartTreePrinter{} // Leading and trailing newlines are added to test cases for readability. We also need to add them to the rendering result. - got := fmt.Sprintf("\n%s\n", RenderSmartTree(tree)) + got := fmt.Sprintf("\n%s\n", printer.Print("root", repos)) // Rendered tree uses spaces for indentation but the test cases use tabs. if got != strings.ReplaceAll(test.want, "\t", " ") {