From cbd1bccbf95ec88419f5efee68835ba4edf1c2d6 Mon Sep 17 00:00:00 2001 From: Norwin Date: Tue, 6 Oct 2020 08:05:22 +0000 Subject: [PATCH] Add `tea repos search`, improve repo listing (#215) Merge branch 'master' into add-repo-search-improve-listing-closes-#210 Merge branch 'master' into add-repo-search-improve-listing-closes-#210 fixup! repos list: client side filtering for repo type fix --private flag repos list: client side filtering for repo type repos list: listing of starred repos repos search: rename --mode to --type repo search: prioritize own user UX tradeoff between usefulness & response speed fix -O owner flag filter rework repo list, add repo search repo search is mostly the old behaviour of repo list Co-authored-by: Lunny Xiao Co-authored-by: Norwin Roosen Reviewed-on: https://gitea.com/gitea/tea/pulls/215 Reviewed-by: 6543 <6543@noreply.gitea.io> Reviewed-by: Lunny Xiao --- cmd/repos.go | 3 +- cmd/repos/flags.go | 35 ++++++++ cmd/repos/list.go | 188 ++++++++++++++++-------------------------- cmd/repos/search.go | 127 ++++++++++++++++++++++++++++ modules/print/repo.go | 39 +++++++++ 5 files changed, 274 insertions(+), 118 deletions(-) create mode 100644 cmd/repos/flags.go create mode 100644 cmd/repos/search.go diff --git a/cmd/repos.go b/cmd/repos.go index 8dbeb3d..8153883 100644 --- a/cmd/repos.go +++ b/cmd/repos.go @@ -25,9 +25,10 @@ var CmdRepos = cli.Command{ Action: runRepos, Subcommands: []*cli.Command{ &repos.CmdReposList, + &repos.CmdReposSearch, &repos.CmdRepoCreate, }, - Flags: flags.LoginOutputFlags, + Flags: repos.CmdReposListFlags, } func runRepos(ctx *cli.Context) error { diff --git a/cmd/repos/flags.go b/cmd/repos/flags.go new file mode 100644 index 0000000..c79b261 --- /dev/null +++ b/cmd/repos/flags.go @@ -0,0 +1,35 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repos + +import ( + "fmt" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +var typeFilterFlag = cli.StringFlag{ + Name: "type", + Aliases: []string{"T"}, + Required: false, + Usage: "Filter by type: fork, mirror, source", +} + +func getTypeFilter(ctx *cli.Context) (filter gitea.RepoType, err error) { + t := ctx.String("type") + filter = gitea.RepoTypeNone + switch t { + case "fork": + filter = gitea.RepoTypeFork + case "mirror": + filter = gitea.RepoTypeMirror + case "source": + filter = gitea.RepoTypeSource + default: + err = fmt.Errorf("invalid repo type '%s'. valid: fork, mirror, source", t) + } + return +} diff --git a/cmd/repos/list.go b/cmd/repos/list.go index e123c63..22f7af2 100644 --- a/cmd/repos/list.go +++ b/cmd/repos/list.go @@ -5,10 +5,6 @@ package repos import ( - "log" - "net/http" - "strings" - "code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/print" @@ -17,39 +13,33 @@ import ( "github.com/urfave/cli/v2" ) +// CmdReposListFlags contains all flags needed for repo listing +var CmdReposListFlags = append([]cli.Flag{ + &cli.BoolFlag{ + Name: "watched", + Aliases: []string{"w"}, + Required: false, + Usage: "List your watched repos instead", + }, + &cli.BoolFlag{ + Name: "starred", + Aliases: []string{"s"}, + Required: false, + Usage: "List your starred repos instead", + }, + &typeFilterFlag, + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, +}, flags.LoginOutputFlags...) + // CmdReposList represents a sub command of repos to list them var CmdReposList = cli.Command{ Name: "ls", Aliases: []string{"list"}, - Usage: "List available repositories", - Description: `List available repositories`, + Usage: "List repositories you have access to", + Description: "List repositories you have access to", Action: RunReposList, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "mode", - Aliases: []string{"m"}, - Required: false, - Usage: "Filter by mode: fork, mirror, source", - }, - &cli.StringFlag{ - Name: "owner", - Aliases: []string{"O"}, - Required: false, - Usage: "Filter by owner", - }, - &cli.StringFlag{ - Name: "private", - Required: false, - Usage: "Filter private repos (true|false)", - }, - &cli.StringFlag{ - Name: "archived", - Required: false, - Usage: "Filter archived repos (true|false)", - }, - &flags.PaginationPageFlag, - &flags.PaginationLimitFlag, - }, flags.LoginOutputFlags...), + Flags: CmdReposListFlags, } // RunReposList list repositories @@ -57,97 +47,61 @@ func RunReposList(ctx *cli.Context) error { login := config.InitCommandLoginOnly(flags.GlobalLoginValue) client := login.Client() - var ownerID int64 - if ctx.IsSet("owner") { - // test if owner is a organisation - org, resp, err := client.GetOrg(ctx.String("owner")) - if err != nil { - if resp == nil || resp.StatusCode != http.StatusNotFound { - return err - } - // if owner is no org, its a user - user, _, err := client.GetUserInfo(ctx.String("owner")) - if err != nil { - return err - } - ownerID = user.ID - } else { - ownerID = org.ID - } - } else { - me, _, err := client.GetMyUserInfo() - if err != nil { - return err - } - ownerID = me.ID - } - - var isArchived *bool - if ctx.IsSet("archived") { - archived := strings.ToLower(ctx.String("archived"))[:1] == "t" - isArchived = &archived - } - - var isPrivate *bool - if ctx.IsSet("private") { - private := strings.ToLower(ctx.String("private"))[:1] == "t" - isArchived = &private - } - - mode := gitea.RepoTypeNone - switch ctx.String("mode") { - case "fork": - mode = gitea.RepoTypeFork - case "mirror": - mode = gitea.RepoTypeMirror - case "source": - mode = gitea.RepoTypeSource - } - - rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ - ListOptions: flags.GetListOptions(ctx), - OwnerID: ownerID, - IsPrivate: isPrivate, - IsArchived: isArchived, - Type: mode, - }) + typeFilter, err := getTypeFilter(ctx) if err != nil { return err } - if len(rps) == 0 { - log.Fatal("No repositories found", rps) - return nil - } - - headers := []string{ - "Name", - "Type", - "SSH", - "Owner", - } - var values [][]string - - for _, rp := range rps { - var mode = "source" - if rp.Fork { - mode = "fork" + var rps []*gitea.Repository + if ctx.Bool("starred") { + user, _, err := client.GetMyUserInfo() + if err != nil { + return err } - if rp.Mirror { - mode = "mirror" - } - - values = append( - values, - []string{ - rp.FullName, - mode, - rp.SSHURL, - rp.Owner.UserName, - }, - ) + rps, _, err = client.SearchRepos(gitea.SearchRepoOptions{ + ListOptions: flags.GetListOptions(ctx), + StarredByUserID: user.ID, + }) + } else if ctx.Bool("watched") { + rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination.. + } else { + rps, _, err = client.ListMyRepos(gitea.ListReposOptions{ + ListOptions: flags.GetListOptions(ctx), + }) } - print.OutputList(flags.GlobalOutputValue, headers, values) + if err != nil { + return err + } + + reposFiltered := rps + if typeFilter != gitea.RepoTypeNone { + reposFiltered = filterReposByType(rps, typeFilter) + } + + print.ReposList(reposFiltered) return nil } + +func filterReposByType(repos []*gitea.Repository, t gitea.RepoType) []*gitea.Repository { + var filtered []*gitea.Repository + for _, r := range repos { + switch t { + case gitea.RepoTypeFork: + if !r.Fork { + continue + } + case gitea.RepoTypeMirror: + if !r.Mirror { + continue + } + case gitea.RepoTypeSource: + if r.Fork || r.Mirror { + continue + } + } + + filtered = append(filtered, r) + } + return filtered +} diff --git a/cmd/repos/search.go b/cmd/repos/search.go new file mode 100644 index 0000000..cf26b32 --- /dev/null +++ b/cmd/repos/search.go @@ -0,0 +1,127 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repos + +import ( + "log" + "strings" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/print" + + "code.gitea.io/sdk/gitea" + "github.com/urfave/cli/v2" +) + +// CmdReposSearch represents a sub command of repos to find them +var CmdReposSearch = cli.Command{ + Name: "search", + Aliases: []string{"s"}, + Usage: "Find any repo on an Gitea instance", + Description: "Find any repo on an Gitea instance", + ArgsUsage: "[]", + Action: runReposSearch, + Flags: append([]cli.Flag{ + &cli.BoolFlag{ + // TODO: it might be nice to search for topics as an ADDITIONAL filter. + // for that, we'd probably need to make multiple queries and UNION the results. + Name: "topic", + Aliases: []string{"t"}, + Required: false, + Usage: "Search for term in repo topics instead of name", + }, + &typeFilterFlag, + &cli.StringFlag{ + Name: "owner", + Aliases: []string{"O"}, + Required: false, + Usage: "Filter by owner", + }, + &cli.StringFlag{ + Name: "private", + Required: false, + Usage: "Filter private repos (true|false)", + }, + &cli.StringFlag{ + Name: "archived", + Required: false, + Usage: "Filter archived repos (true|false)", + }, + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, + }, flags.LoginOutputFlags...), +} + +func runReposSearch(ctx *cli.Context) error { + login := config.InitCommandLoginOnly(flags.GlobalLoginValue) + client := login.Client() + + var ownerID int64 + if ctx.IsSet("owner") { + // test if owner is a organisation + org, _, err := client.GetOrg(ctx.String("owner")) + if err != nil { + // HACK: the client does not return a response on 404, so we can't check res.StatusCode + if err.Error() != "404 Not Found" { + log.Fatal("could not find owner: ", err) + } + + // if owner is no org, its a user + user, _, err := client.GetUserInfo(ctx.String("owner")) + if err != nil { + return err + } + ownerID = user.ID + } else { + ownerID = org.ID + } + } + + var isArchived *bool + if ctx.IsSet("archived") { + archived := strings.ToLower(ctx.String("archived"))[:1] == "t" + isArchived = &archived + } + + var isPrivate *bool + if ctx.IsSet("private") { + private := strings.ToLower(ctx.String("private"))[:1] == "t" + isPrivate = &private + } + + mode, err := getTypeFilter(ctx) + if err != nil { + return err + } + + var keyword string + if ctx.Args().Present() { + keyword = strings.Join(ctx.Args().Slice(), " ") + } + + user, _, err := client.GetMyUserInfo() + if err != nil { + return err + } + + rps, _, err := client.SearchRepos(gitea.SearchRepoOptions{ + ListOptions: flags.GetListOptions(ctx), + OwnerID: ownerID, + IsPrivate: isPrivate, + IsArchived: isArchived, + Type: mode, + Keyword: keyword, + KeywordInDescription: true, + KeywordIsTopic: ctx.Bool("topic"), + PrioritizedByOwnerID: user.ID, + }) + if err != nil { + return err + } + + print.ReposList(rps) + return nil +} diff --git a/modules/print/repo.go b/modules/print/repo.go index 96ee374..e8b48b7 100644 --- a/modules/print/repo.go +++ b/modules/print/repo.go @@ -9,8 +9,47 @@ import ( "strings" "code.gitea.io/sdk/gitea" + "code.gitea.io/tea/cmd/flags" ) +// ReposList prints a listing of the repos +func ReposList(rps []*gitea.Repository) { + if len(rps) == 0 { + fmt.Println("No repositories found") + return + } + + headers := []string{ + "Name", + "Type", + "SSH", + "Owner", + } + var values [][]string + + for _, rp := range rps { + var mode = "source" + if rp.Fork { + mode = "fork" + } + if rp.Mirror { + mode = "mirror" + } + + values = append( + values, + []string{ + rp.FullName, + mode, + rp.SSHURL, + rp.Owner.UserName, + }, + ) + } + + OutputList(flags.GlobalOutputValue, headers, values) +} + // RepoDetails print an repo formatted to stdout func RepoDetails(repo *gitea.Repository, topics []string) { output := repo.FullName