Skip to content

Commit

Permalink
glob matcher for view cmd; cmd aliases (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
sarumaj authored Jan 24, 2024
1 parent 39c4732 commit c535a4a
Show file tree
Hide file tree
Showing 18 changed files with 203 additions and 101 deletions.
5 changes: 3 additions & 2 deletions pkg/commands/command_cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
)

var cleanupCmd = &cobra.Command{
Use: "cleanup",
Short: "Clean up untracked local repositories",
Use: "cleanup",
Aliases: []string{"clean", "cl"},
Short: "Clean up untracked local repositories",
Long: "Clean up untracked local repositories.\n\n" +
"Multiple selection is possible (default: all).",
Example: "gh gr cleanup",
Expand Down
9 changes: 5 additions & 4 deletions pkg/commands/command_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (

var initCmd = func() *cobra.Command {
initCmd := &cobra.Command{
Use: "init",
Short: "Initialize repository mirror",
Use: "init",
Aliases: []string{"setup"},
Short: "Initialize repository mirror",
Long: "Initialize repository mirror.\n\n" +
"Automatically generates a list of repositories a given user has permissions to.\n" +
"Supports filtering by repository blob size and with regular expressions.\n" +
Expand Down Expand Up @@ -40,8 +41,8 @@ var initCmd = func() *cobra.Command {
flags.StringVarP(&configFlags.BaseDirectory, "dir", "d", ".", "Directory in which repositories will be stored (either absolute or relative)")
flags.BoolVarP(&configFlags.SubDirectories, "subdirs", "s", false, "Enable creation of separate subdirectories for each org/user")
flags.Uint64VarP(&configFlags.SizeLimit, "sizelimit", "l", 0, "Exclude repositories with size exceeded the limit (\"0\": no limit, e.g. limit of 52,428,800 corresponds with 50 MB)")
flags.StringArrayVarP(&configFlags.Excluded, "exclude", "e", []string{}, "Regular expressions of repositories to exclude")
flags.StringArrayVarP(&configFlags.Included, "include", "i", []string{}, "Regular expressions of repositories to include explicitly")
flags.StringArrayVarP(&configFlags.Excluded, "exclude", "e", []string{}, "Regular expressions for repositories to exclude")
flags.StringArrayVarP(&configFlags.Included, "include", "i", []string{}, "Regular expressions for repositories to include explicitly")

return initCmd
}()
12 changes: 5 additions & 7 deletions pkg/commands/command_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ func pullOperation(_ pool.WorkUnit, args operationContext) {

logger := loggerEntry.WithField("command", "pull").WithField("repository", repo.Directory)

conf.Authenticate(&repo.URL)
conf.Authenticate(&repo.ParentURL)
conf.AuthenticateURL(&repo.URL)
conf.AuthenticateURL(&repo.ParentURL)
logger.Debugf("Authenticated: URL: %t, ParentURL: %t", repo.URL != "", repo.ParentURL != "")

defer util.Chdir(conf.AbsoluteDirectoryPath).Popd()
Expand Down Expand Up @@ -255,17 +255,15 @@ func updateRepoConfig(conf *configfile.Configuration, host string, repository *g
// update remote "origin" urls to use current authentication context
if cfg, ok := repoConf.Remotes["origin"]; ok {
for i := range cfg.URLs {
conf.Generalize(&cfg.URLs[i])
conf.Authenticate(&cfg.URLs[i])
conf.AuthenticateURL(&cfg.URLs[i])
}

repoConf.Remotes["origin"] = cfg
}

// update submodules urls to use current authentication context
// update submodules' urls to use current authentication context
for name, cfg := range repoConf.Submodules {
conf.Generalize(&cfg.URL)
conf.Authenticate(&cfg.URL)
conf.AuthenticateURL(&cfg.URL)
repoConf.Submodules[name] = cfg
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/commands/command_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func pushOperation(_ pool.WorkUnit, args operationContext) {

logger := loggerEntry.WithField("command", "push").WithField("repository", repo.Directory)

conf.Authenticate(&repo.URL)
conf.Authenticate(&repo.ParentURL)
conf.AuthenticateURL(&repo.URL)
conf.AuthenticateURL(&repo.ParentURL)
logger.Debugf("Authenticated: URL: %t, ParentURL: %t", repo.URL != "", repo.ParentURL != "")

defer util.Chdir(conf.AbsoluteDirectoryPath).Popd()
Expand Down
5 changes: 3 additions & 2 deletions pkg/commands/command_remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ var removeCmd = func() *cobra.Command {
var purge bool

removeCmd := &cobra.Command{
Use: "remove",
Short: "Remove current configuration",
Use: "remove",
Aliases: []string{"reset", "rm", "delete", "del"},
Short: "Remove current configuration",
Long: "Remove current configuration.\n\n" +
"To remove local repositories as well, provide the \"--purge\" option.",
Example: "gh gr remove --purge",
Expand Down
4 changes: 2 additions & 2 deletions pkg/commands/command_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func statusOperation(_ pool.WorkUnit, args operationContext) {

logger := loggerEntry.WithField("command", "status").WithField("repository", repo.Directory)

conf.Authenticate(&repo.URL)
conf.Authenticate(&repo.ParentURL)
conf.AuthenticateURL(&repo.URL)
conf.AuthenticateURL(&repo.ParentURL)
logger.Debugf("Authenticated: URL: %t, ParentURL: %t", repo.URL != "", repo.ParentURL != "")

defer util.Chdir(conf.AbsoluteDirectoryPath).Popd()
Expand Down
6 changes: 4 additions & 2 deletions pkg/commands/command_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (

var viewCmd = func() *cobra.Command {
var formatOption string
var filters []string

viewCmd := &cobra.Command{
Aliases: []string{"show", "list"},
Aliases: []string{"show", "list", "ls"},
Use: "view",
Short: "Display current configuration",
Long: "Display current configuration.\n\n" +
Expand All @@ -30,13 +31,14 @@ var viewCmd = func() *cobra.Command {
conf := configfile.Load()

logger.Debug("Streaming")
conf.Display(formatOption, false)
conf.Display(formatOption, false, filters...)
},
}

flags := viewCmd.Flags()
supportedFormats := strings.Join(configfile.GetListOfSupportedFormats(true), ", ")
flags.StringVarP(&formatOption, "format", "f", "yaml", fmt.Sprintf("Change output format, supported formats: [%s]", supportedFormats))
flags.StringArrayVarP(&filters, "match", "m", []string{}, "Glob pattern(s) to filter repositories")

return viewCmd
}()
26 changes: 18 additions & 8 deletions pkg/configfile/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ type Configuration struct {
Repositories Repositories `json:"repositories" yaml:"repositories"`
}

// Append multiple repositories and sort them alphabetically by Directory.
// AppendRepositories appends multiple repositories to the configuration and sorts them alphabetically by Directory.
func (conf *Configuration) AppendRepositories(user *resources.User, repos ...resources.Repository) {
for _, repo := range repos {
dir := repo.FullName
Expand Down Expand Up @@ -125,9 +125,9 @@ func (conf *Configuration) AppendRepositories(user *resources.User, repos ...res
loggerEntry.Debugf("Configured %d repositories", conf.Total)
}

// Encode username and token into URL.
// AuthenticateURL encodes username and token into URL.
// In the case, no matching token can be found for given URL, emit message and exit.
func (conf Configuration) Authenticate(targetURL *string) {
func (conf Configuration) AuthenticateURL(targetURL *string) {
loggerEntry.Debugf("Authenticating URL: %v", targetURL)
if targetURL == nil || *targetURL == "" || !urlRegex.MatchString(*targetURL) {
loggerEntry.Debugf("Got empty or invalid URL: %#v", targetURL)
Expand Down Expand Up @@ -257,7 +257,7 @@ func (conf *Configuration) Copy() *Configuration {

// Flush config into Stdout.
// Supports multiple formats and partial emission (if !export).
func (conf Configuration) Display(format string, export bool) {
func (conf Configuration) Display(format string, export bool, filters ...string) {
reader, writer := io.Pipe()
c := util.Console()

Expand All @@ -273,6 +273,16 @@ func (conf Configuration) Display(format string, export bool) {
defer func(w io.Writer) { util.Logger.SetOutput(w) }(out)
}

if len(filters) > 0 {
var repositories []Repository
for _, repository := range conf.Repositories {
if util.PatternList(filters).GlobMatch(util.StripPathPrefix(repository.Directory, 1)) {
repositories = append(repositories, repository)
}
}
conf.Repositories = repositories
}

go func() {
defer writer.Close()
supererrors.Except(enc.Encoder(writer, !export && c.ColorsEnabled()).Encode(conf))
Expand Down Expand Up @@ -319,9 +329,9 @@ func (conf *Configuration) FilterRepositories(repositories *[]resources.Reposito

case
// not explicitly included
len(conf.Included) > 0 && !util.RegexList(conf.Included).Match(repo.FullName, conf.Timeout),
len(conf.Included) > 0 && !util.PatternList(conf.Included).RegexMatch(repo.FullName, conf.Timeout),
// explicitly excluded
len(conf.Excluded) > 0 && util.RegexList(conf.Excluded).Match(repo.FullName, conf.Timeout),
len(conf.Excluded) > 0 && util.PatternList(conf.Excluded).RegexMatch(repo.FullName, conf.Timeout),
// repository size exceeds size limit
conf.SizeLimit > 0 && uint64(repo.Size) > conf.SizeLimit,
// repository is archived or disabled
Expand All @@ -339,8 +349,8 @@ func (conf *Configuration) FilterRepositories(repositories *[]resources.Reposito
}
}

// Remove username and token from URL.
func (conf Configuration) Generalize(targetURL *string) {
// GeneralizeURL removes username and token from URL.
func (conf Configuration) GeneralizeURL(targetURL *string) {
if targetURL == nil || *targetURL == "" || !urlRegex.MatchString(*targetURL) {
loggerEntry.Debugf("Got empty or invalid URL: %#v", targetURL)
return
Expand Down
8 changes: 4 additions & 4 deletions pkg/configfile/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ gr.conf: |
branch: main
`

func TestConfigurationAuthenticate(t *testing.T) {
func TestConfigurationAuthenticateURL(t *testing.T) {
conf := &Configuration{
Profiles: Profiles{{Username: "user", Host: "example.com"}},
}
Expand All @@ -63,7 +63,7 @@ func TestConfigurationAuthenticate(t *testing.T) {
} {
t.Run(tt.name, func(t *testing.T) {
got := tt.args
conf.Authenticate(&got)
conf.AuthenticateURL(&got)
if got != tt.want {
t.Errorf(`conf.Authenticate(&(%q)) failed: got: %q, want %q`, tt.args, got, tt.want)
}
Expand Down Expand Up @@ -155,11 +155,11 @@ func TestConfigurationGeneralize(t *testing.T) {
{"test#1", "", ""},
{"test#2", "https://user:pass@example.com", "https://example.com"},
{"test#3", "https://example.com", "https://example.com"},
{"test#4", "https://example.com/q=1", "https://example.com/q=1"},
{"test#4", "https://example.com?q=1", "https://example.com?q=1"},
} {
t.Run(tt.name, func(t *testing.T) {
got := tt.args
conf.Generalize(&got)
conf.GeneralizeURL(&got)
if got != tt.want {
t.Errorf(`conf.Generalize(&(%q)) failed: got: %q, want %q`, tt.args, got, tt.want)
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/restclient/rest_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (c RESTClient) DoWithContext(ctx context.Context, method string, path strin
}

// Get all repositories for given user and the organizations he belongs to.
func (c RESTClient) GetAllUserRepos(ctx context.Context, include, exclude util.RegexList) ([]resources.Repository, error) {
func (c RESTClient) GetAllUserRepos(ctx context.Context, include, exclude []string) ([]resources.Repository, error) {
repos, err := c.GetUserRepos(ctx)
if err != nil {
return nil, err
Expand All @@ -49,10 +49,10 @@ func (c RESTClient) GetAllUserRepos(ctx context.Context, include, exclude util.R
}

for _, org := range orgs {
switch {
switch includes, excludes := util.PatternList(include), util.PatternList(exclude); {
case
len(include) > 0 && !(include.Match(org.Login+"/someRepository", timeout) || include.Match(org.Login+"/", timeout)),
len(exclude) > 0 && (exclude.Match(org.Login+"/someRepository", timeout) || exclude.Match(org.Login+"/", timeout)):
len(include) > 0 && !(includes.RegexMatch(org.Login+"/someRepository", timeout) || includes.RegexMatch(org.Login+"/", timeout)),
len(exclude) > 0 && (excludes.RegexMatch(org.Login+"/someRepository", timeout) || excludes.RegexMatch(org.Login+"/", timeout)):

continue
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/util/colored_yaml_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"io"
"regexp"

"github.com/fatih/color"
color "github.com/fatih/color"
yaml "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/lexer"
"github.com/goccy/go-yaml/printer"
lexer "github.com/goccy/go-yaml/lexer"
printer "github.com/goccy/go-yaml/printer"
)

// Regular expression for matching ANSI code sequences for color codes.
Expand Down
2 changes: 1 addition & 1 deletion pkg/util/environ.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func Getenv(key envVariable) string { return os.Getenv(EnvPrefix + string(key))

// Retrieve environment variable of boolean type.
func GetenvBool(key envVariable) bool {
switch Getenv(Verbose) {
switch Getenv(key) {
case "true", "TRUE", "True", "1", "Y", "y", "YES", "yes":
return true

Expand Down
23 changes: 23 additions & 0 deletions pkg/util/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,26 @@ func PathSanitize(paths ...*string) {
*path = strings.TrimSuffix(*path, "/")
}
}

// StripPathPrefix strips prefix from path given a number of parent directories to keep.
func StripPathPrefix(path string, keepParents uint) string {
if path == "" {
return filepath.FromSlash("")
}

if p, err := filepath.Abs(path); err == nil {
path = p
}

if length := len(filepath.SplitList(path)); length > int(keepParents) {
keepParents = uint(length)
}

parts := []string{filepath.Base(path)}
for i := uint(0); i < keepParents; i++ {
path = filepath.Dir(path)
parts = append([]string{filepath.Base(path)}, parts...)
}

return filepath.Join(parts...)
}
26 changes: 26 additions & 0 deletions pkg/util/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ func TestPathSanitize(t *testing.T) {
})
}
}

func TestStripPathPrefix(t *testing.T) {
type args struct {
path string
keepParents uint
}

for _, tt := range []struct {
name string
args args
want string
}{
{"test#1", args{"", 0}, ""},
{"test#2", args{"/dir1/dir2/item", 1}, "dir2/item"},
{"test#3", args{"/dir1/dir2/item", 2}, "dir1/dir2/item"},
{"test#4", args{"/dir1/../dir2/item", 1}, "dir2/item"},
{"test#5", args{"/dir1/../dir2/item", 10}, "/dir2/item"},
} {
t.Run(tt.name, func(t *testing.T) {
got := StripPathPrefix(tt.args.path, tt.args.keepParents)
if got != tt.want {
t.Errorf(`StripPathPrefix(%q, %d) failed: got: %q, want: %q`, tt.args.path, tt.args.keepParents, got, tt.want)
}
})
}
}
47 changes: 47 additions & 0 deletions pkg/util/pattern_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package util

import (
"path/filepath"
"time"

regexp2 "github.com/dlclark/regexp2"
)

// PatternList is a list of regular expressions.
type PatternList []string

// GlobMatch checks if target matches any of the globs in the list.
func (l PatternList) GlobMatch(target string) bool {
for _, pattern := range l {
ok, err := filepath.Match(pattern, target)
if err != nil {
continue
}

if ok {
return true
}
}

return false
}

// RegexMatch checks if target matches any of the regular expressions in the list.
func (l PatternList) RegexMatch(target string, timeout time.Duration) bool {
for _, pattern := range l {
re, err := regexp2.Compile(pattern, regexp2.RE2)
if err != nil {
continue
}

if timeout > 0 {
re.MatchTimeout = timeout
}

if match, err := re.MatchString(target); err == nil && match {
return true
}
}

return false
}
Loading

0 comments on commit c535a4a

Please sign in to comment.