diff --git a/cmd/projects/branches.go b/cmd/projects/branches.go index 4a3a7a5..858a810 100644 --- a/cmd/projects/branches.go +++ b/cmd/projects/branches.go @@ -6,7 +6,6 @@ import ( "strings" "text/tabwriter" - "dario.cat/mergo" "github.com/flant/glaball/cmd/common" "github.com/flant/glaball/pkg/client" "github.com/flant/glaball/pkg/limiter" @@ -18,29 +17,23 @@ import ( ) const ( - protectedBranchDefaultField = "project.web_url" + branchDefaultField = "project.web_url" ) var ( - listProtectedBranchesOptions = gitlab.ListProtectedBranchesOptions{ListOptions: gitlab.ListOptions{PerPage: 100}} - protectRepositoryBranchesOptions = gitlab.ProtectRepositoryBranchesOptions{} - protectedBranchOrderBy []string - forceProtect bool - protectedBranchFormat = util.Dict{ + listBranchesOptions = gitlab.ListBranchesOptions{ListOptions: gitlab.ListOptions{PerPage: 100}} + branchOrderBy []string + branchFormat = util.Dict{ { - Key: "COUNT", - Value: "[%d]", - }, - { - Key: "REPOSITORY", - Value: "%s", + Key: "HOST", + Value: "[%s]", }, { - Key: "BRANCHES", + Key: "URL", Value: "%s", }, { - Key: "HOST", + Key: "LAST UPDATED", Value: "[%s]", }, { @@ -57,133 +50,42 @@ func NewBranchesCmd() *cobra.Command { } cmd.AddCommand( + NewBranchesListCmd(), NewProtectedBranchesCmd(), ) return cmd } -func NewProtectedBranchesCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "protected", - Short: "Protected branches API", - } - - cmd.AddCommand( - NewProtectedBranchesListCmd(), - NewProtectRepositoryBranchesCmd(), - ) - - return cmd -} - -func NewProtectedBranchesListCmd() *cobra.Command { +func NewBranchesListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", - Short: "List protected branches", - Long: "Gets a list of protected branches from a project as they are defined in the UI. If a wildcard is set, it is returned instead of the exact name of the branches that match that wildcard.", + Short: "List repository branches", + Long: "Get a list of repository branches from a project, sorted by name alphabetically.", RunE: func(cmd *cobra.Command, args []string) error { - return ProtectedBranchesListCmd() + return BranchesListCmd() }, } cmd.Flags().Var(util.NewEnumValue(&sortBy, "asc", "desc"), "sort", - "Return protected branches sorted in asc or desc order. Default is desc") - - cmd.Flags().StringSliceVar(&protectedBranchOrderBy, "order_by", []string{"count", protectedBranchDefaultField}, - `Return protected branches ordered by web_url, created_at, title, updated_at or any nested field. Default is web_url.`) - - listProjectsOptionsFlags(cmd, &listProjectsOptions) - - return cmd -} + "Return branches sorted in asc or desc order. Default is desc") -func NewProtectRepositoryBranchesCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "protect", - Short: "Protect repository branches", - Long: "Protects a single repository branch or several project repository branches using a wildcard protected branch.", - RunE: func(cmd *cobra.Command, args []string) error { - return ProtectRepositoryBranchesCmd() - }, - } - - cmd.Flags().BoolVar(&forceProtect, "force", false, - "Force update already protected branches.") - - cmd.Flags().Var(util.NewStringPtrValue(&protectRepositoryBranchesOptions.Name), "name", - "The name of the branch or wildcard") - - cmd.Flags().Var(util.NewAccessLevelValue(&protectRepositoryBranchesOptions.PushAccessLevel), "push_access_level", - "Access levels allowed to push (defaults: 40, Maintainer role)") - - cmd.Flags().Var(util.NewAccessLevelValue(&protectRepositoryBranchesOptions.MergeAccessLevel), "merge_access_level", - "Access levels allowed to merge (defaults: 40, Maintainer role)") - - cmd.Flags().Var(util.NewAccessLevelValue(&protectRepositoryBranchesOptions.UnprotectAccessLevel), "unprotect_access_level", - "Access levels allowed to unprotect (defaults: 40, Maintainer role)") - - cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowForcePush), "allow_force_push", - "Allow all users with push access to force push. (default: false)") - - // cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowedToPush), "allowed_to_push", - // "Array of access levels allowed to push, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}") - - // cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowedToMerge), "allowed_to_merge", - // "Array of access levels allowed to merge, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}") - - // cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowedToUnprotect), "allowed_to_unprotect", - // "Array of access levels allowed to unprotect, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}") - - cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.CodeOwnerApprovalRequired), "code_owner_approval_required", - "Prevent pushes to this branch if it matches an item in the CODEOWNERS file. (defaults: false)") - - cmd.MarkFlagRequired("name") + cmd.Flags().StringSliceVar(&branchOrderBy, "order_by", []string{"count", branchDefaultField}, + `Return branches ordered by web_url, created_at, title, updated_at or any nested field. Default is web_url.`) listProjectsOptionsFlags(cmd, &listProjectsOptions) return cmd } -type ProjectProtectedBranch struct { - Project *gitlab.Project `json:"project,omitempty"` - ProtectedBranches []*gitlab.ProtectedBranch `json:"protected_branches,omitempty"` +type ProjectBranch struct { + Project *gitlab.Project `json:"project,omitempty"` + Branches []*gitlab.Branch `json:"branches,omitempty"` } -func (pb *ProjectProtectedBranch) BranchesNames() []string { - switch len(pb.ProtectedBranches) { - case 0: - return []string{} - case 1: - return []string{pb.ProtectedBranches[0].Name} - } - - branches := make([]string, 0, len(pb.ProtectedBranches)) - for _, b := range pb.ProtectedBranches { - branches = util.InsertString(branches, b.Name) - } - return branches -} - -func (pb *ProjectProtectedBranch) Search(name string) (*gitlab.ProtectedBranch, bool) { - switch len(pb.ProtectedBranches) { - case 0: - return nil, false - case 1: - return pb.ProtectedBranches[0], pb.ProtectedBranches[0].Name == name - } - // linear search - for _, b := range pb.ProtectedBranches { - if b.Name == name { - return b, true - } - } - return nil, false -} - -func ProtectedBranchesListCmd() error { - if !sort.ValidOrderBy(protectedBranchOrderBy, ProjectProtectedBranch{}) { - protectedBranchOrderBy = append(protectedBranchOrderBy, protectedBranchDefaultField) +func BranchesListCmd() error { + if !sort.ValidOrderBy(branchOrderBy, ProjectBranch{}) { + branchOrderBy = append(branchOrderBy, branchDefaultField) } wg := common.Limiter @@ -195,7 +97,7 @@ func ProtectedBranchesListCmd() error { }() for _, h := range common.Client.Hosts { - fmt.Printf("Getting protected branches from %s ...\n", h.URL) + fmt.Printf("Getting branches from %s ...\n", h.URL) wg.Add(1) go listProjects(h, listProjectsOptions, wg, data, common.Client.WithCache()) } @@ -214,33 +116,33 @@ func ProtectedBranchesListCmd() error { return fmt.Errorf("no projects found") } - protectedBranches := make(chan interface{}) + branches := make(chan interface{}) for _, v := range toList.Typed() { wg.Add(1) - go listProtectedBranches(v.Host, v.Struct.(*gitlab.Project), listProtectedBranchesOptions, wg, protectedBranches, common.Client.WithCache()) + go listBranches(v.Host, v.Struct.(*gitlab.Project), listBranchesOptions, wg, branches, common.Client.WithCache()) } go func() { wg.Wait() - close(protectedBranches) + close(branches) }() - results, err := sort.FromChannel(protectedBranches, &sort.Options{ - OrderBy: protectedBranchOrderBy, + results, err := sort.FromChannel(branches, &sort.Options{ + OrderBy: branchOrderBy, SortBy: sortBy, - GroupBy: protectedBranchDefaultField, - StructType: ProjectProtectedBranch{}, + GroupBy: branchDefaultField, + StructType: ProjectBranch{}, }) if err != nil { return err } if len(results) == 0 { - return fmt.Errorf("no protected branches found") + return fmt.Errorf("no branches found") } w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) - if _, err := fmt.Fprintln(w, strings.Join(protectedBranchFormat.Keys(), "\t")); err != nil { + if _, err := fmt.Fprintln(w, strings.Join(branchFormat.Keys(), "\t")); err != nil { return err } @@ -251,135 +153,18 @@ func ProtectedBranchesListCmd() error { unique++ total += r.Count for _, v := range r.Elements.Typed() { - if err := protectedBranchFormat.Print(w, "\t", - r.Count, - r.Key, - v.Struct.(*ProjectProtectedBranch).BranchesNames(), - v.Host.ProjectName(), - r.Cached, - ); err != nil { - return err + pb := v.Struct.(*ProjectBranch) + for _, b := range pb.Branches { + if err := branchFormat.Print(w, "\t", + v.Host.ProjectName(), + b.WebURL, + b.Commit.CommittedDate.Format("2006-01-02 15:04:05"), + v.Cached, + ); err != nil { + return err + } } - } - - } - - if err := totalFormat.Print(w, "\n", unique, total, len(wg.Errors())); err != nil { - return err - } - - if err := w.Flush(); err != nil { - return err - } - - return nil -} - -func ProtectRepositoryBranchesCmd() error { - if !sort.ValidOrderBy(protectedBranchOrderBy, ProjectProtectedBranch{}) { - protectedBranchOrderBy = append(protectedBranchOrderBy, protectedBranchDefaultField) - } - wg := common.Limiter - data := make(chan interface{}) - defer func() { - for _, err := range wg.Errors() { - hclog.L().Error(err.Err.Error()) - } - }() - - for _, h := range common.Client.Hosts { - fmt.Printf("Getting protected branches from %s ...\n", h.URL) - wg.Add(1) - go listProjects(h, listProjectsOptions, wg, data, common.Client.WithCache()) - } - - go func() { - wg.Wait() - close(data) - }() - - toList := make(sort.Elements, 0) - for e := range data { - toList = append(toList, e) - } - - if len(toList) == 0 { - return fmt.Errorf("no projects found") - } - - protectedBranches := make(chan interface{}) - for _, v := range toList.Typed() { - wg.Add(1) - go listProtectedBranches(v.Host, v.Struct.(*gitlab.Project), listProtectedBranchesOptions, wg, protectedBranches, common.Client.WithCache()) - } - - go func() { - wg.Wait() - close(protectedBranches) - }() - - toProtect := make(sort.Elements, 0) - for e := range protectedBranches { - if v := e.(sort.Element).Struct.(*ProjectProtectedBranch); forceProtect || len(v.ProtectedBranches) == 0 { - toProtect = append(toProtect, e) - } - } - - if len(toProtect) == 0 { - return fmt.Errorf("branch %q is already protected in %d repositories in %v", - *protectRepositoryBranchesOptions.Name, len(toList), common.Client.Hosts.Projects(common.Config.ShowAll)) - } - - util.AskUser(fmt.Sprintf("Do you really want to protect branch %q in %d repositories in %v ?", - *protectRepositoryBranchesOptions.Name, len(toProtect), common.Client.Hosts.Projects(common.Config.ShowAll))) - - protectedCh := make(chan interface{}) - for _, v := range toProtect.Typed() { - wg.Add(1) - go protectRepositoryBranches(v.Host, v.Struct.(*ProjectProtectedBranch), forceProtect, protectRepositoryBranchesOptions, wg, protectedCh, common.Client.WithNoCache()) - } - - go func() { - wg.Wait() - close(protectedCh) - }() - - results, err := sort.FromChannel(protectedCh, &sort.Options{ - OrderBy: protectedBranchOrderBy, - SortBy: sortBy, - GroupBy: protectedBranchDefaultField, - StructType: ProjectProtectedBranch{}, - }) - if err != nil { - return err - } - - if len(results) == 0 { - return fmt.Errorf("no protected branches found") - } - - w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) - if _, err := fmt.Fprintln(w, strings.Join(protectedBranchFormat.Keys(), "\t")); err != nil { - return err - } - - unique := 0 - total := 0 - - for _, r := range results { - unique++ - total += r.Count - for _, v := range r.Elements.Typed() { - if err := protectedBranchFormat.Print(w, "\t", - r.Count, - r.Key, - v.Struct.(*ProjectProtectedBranch).BranchesNames(), - v.Host.ProjectName(), - r.Cached, - ); err != nil { - return err - } } } @@ -395,13 +180,13 @@ func ProtectRepositoryBranchesCmd() error { return nil } -func listProtectedBranches(h *client.Host, project *gitlab.Project, opt gitlab.ListProtectedBranchesOptions, +func listBranches(h *client.Host, project *gitlab.Project, opt gitlab.ListBranchesOptions, wg *limiter.Limiter, data chan<- interface{}, options ...gitlab.RequestOptionFunc) error { defer wg.Done() wg.Lock() - list, resp, err := h.Client.ProtectedBranches.ListProtectedBranches(project.ID, &opt, options...) + list, resp, err := h.Client.Branches.ListBranches(project.ID, &opt, options...) wg.Unlock() if err != nil { wg.Error(h, err) @@ -409,112 +194,15 @@ func listProtectedBranches(h *client.Host, project *gitlab.Project, opt gitlab.L } data <- sort.Element{ - Host: h, - Struct: &ProjectProtectedBranch{ - Project: project, - ProtectedBranches: list}, + Host: h, + Struct: &ProjectBranch{Project: project, Branches: list}, Cached: resp.Header.Get("X-From-Cache") == "1"} if resp.NextPage > 0 { wg.Add(1) opt.Page = resp.NextPage - go listProtectedBranches(h, project, opt, wg, data, options...) + go listBranches(h, project, opt, wg, data, options...) } return nil } - -func protectRepositoryBranches(h *client.Host, pb *ProjectProtectedBranch, forceProtect bool, opt gitlab.ProtectRepositoryBranchesOptions, - wg *limiter.Limiter, data chan<- interface{}, options ...gitlab.RequestOptionFunc) error { - - defer wg.Done() - - if forceProtect { - if old, ok := pb.Search(*opt.Name); ok { - new := opt - - new.AllowForcePush = &old.AllowForcePush - new.CodeOwnerApprovalRequired = &old.CodeOwnerApprovalRequired - - switch n := len(old.MergeAccessLevels); n { - case 0: - case 1: - new.MergeAccessLevel = &old.MergeAccessLevels[0].AccessLevel - default: - allowedToMerge := make([]*gitlab.BranchPermissionOptions, 0, n) - for _, l := range old.MergeAccessLevels { - allowedToMerge = append(allowedToMerge, &gitlab.BranchPermissionOptions{ - UserID: &l.UserID, - GroupID: &l.GroupID, - AccessLevel: &l.AccessLevel, - }) - } - new.AllowedToMerge = &allowedToMerge - } - - switch n := len(old.PushAccessLevels); n { - case 0: - case 1: - new.PushAccessLevel = &old.PushAccessLevels[0].AccessLevel - default: - allowedToPush := make([]*gitlab.BranchPermissionOptions, 0, n) - for _, l := range old.PushAccessLevels { - allowedToPush = append(allowedToPush, &gitlab.BranchPermissionOptions{ - UserID: &l.UserID, - GroupID: &l.GroupID, - AccessLevel: &l.AccessLevel, - }) - } - new.AllowedToPush = &allowedToPush - } - - switch n := len(old.UnprotectAccessLevels); n { - case 0: - case 1: - new.UnprotectAccessLevel = &old.UnprotectAccessLevels[0].AccessLevel - default: - allowedToUnprotect := make([]*gitlab.BranchPermissionOptions, 0, n) - for _, l := range old.UnprotectAccessLevels { - allowedToUnprotect = append(allowedToUnprotect, &gitlab.BranchPermissionOptions{ - UserID: &l.UserID, - GroupID: &l.GroupID, - AccessLevel: &l.AccessLevel, - }) - } - new.AllowedToUnprotect = &allowedToUnprotect - } - - if err := mergo.Merge(&new, opt, mergo.WithOverwriteWithEmptyValue); err != nil { - wg.Error(h, err) - return err - } - - wg.Lock() - _, err := h.Client.ProtectedBranches.UnprotectRepositoryBranches(pb.Project.ID, *new.Name, options...) - wg.Unlock() - if err != nil { - wg.Error(h, err) - return err - } - - opt = new - } - } - - wg.Lock() - v, resp, err := h.Client.ProtectedBranches.ProtectRepositoryBranches(pb.Project.ID, &opt, options...) - wg.Unlock() - if err != nil { - wg.Error(h, err) - return err - } - - data <- sort.Element{ - Host: h, - Struct: &ProjectProtectedBranch{ - Project: pb.Project, - ProtectedBranches: []*gitlab.ProtectedBranch{v}}, - Cached: resp.Header.Get("X-From-Cache") == "1"} - - return nil -} diff --git a/cmd/projects/mr.go b/cmd/projects/mr.go index 5860289..c35b805 100644 --- a/cmd/projects/mr.go +++ b/cmd/projects/mr.go @@ -145,6 +145,9 @@ func MergeRequestsListCmd() error { for _, v := range results { for _, elem := range v.Elements.Typed() { mr := elem.Struct.(*gitlab.MergeRequest) + if mr.Author == nil { + mr.Author = &gitlab.BasicUser{Username: "-"} + } w.Write([]string{elem.Host.Project, mr.WebURL, mr.Title, mr.Author.Username, mr.UpdatedAt.Format("2006-01-02 15:04:05")}) } } @@ -153,14 +156,21 @@ func MergeRequestsListCmd() error { if util.ContainsString(outputFormat, "table") { w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) - fmt.Fprintf(w, "HOST\tURL\tAuthor\tLast Updated\n") + fmt.Fprintf(w, "HOST\tTitle\tURL\tAuthor\tLast Updated\n") total := 0 for _, v := range results { for _, elem := range v.Elements.Typed() { total++ mr := elem.Struct.(*gitlab.MergeRequest) - fmt.Fprintf(w, "[%s]\t%s\t[%s]\t%s\n", elem.Host.Project, mr.WebURL, mr.Author.Username, mr.UpdatedAt.Format("2006-01-02 15:04:05")) + if mr.Author == nil { + mr.Author = &gitlab.BasicUser{Username: "-"} + } + if !common.Config.ShowAll && len(mr.Title) > 64 { + mr.Title = mr.Title[:64] + "..." + } + + fmt.Fprintf(w, "[%s]\t%s\t%s\t[%s]\t%s\n", elem.Host.Project, mr.Title, mr.WebURL, mr.Author.Username, mr.UpdatedAt.Format("2006-01-02 15:04:05")) } } diff --git a/cmd/projects/protected_branches.go b/cmd/projects/protected_branches.go new file mode 100644 index 0000000..bac7c8e --- /dev/null +++ b/cmd/projects/protected_branches.go @@ -0,0 +1,507 @@ +package projects + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + + "dario.cat/mergo" + "github.com/flant/glaball/cmd/common" + "github.com/flant/glaball/pkg/client" + "github.com/flant/glaball/pkg/limiter" + "github.com/flant/glaball/pkg/sort/v2" + "github.com/flant/glaball/pkg/util" + "github.com/hashicorp/go-hclog" + "github.com/spf13/cobra" + "github.com/xanzy/go-gitlab" +) + +const ( + protectedBranchDefaultField = "project.web_url" +) + +var ( + listProtectedBranchesOptions = gitlab.ListProtectedBranchesOptions{ListOptions: gitlab.ListOptions{PerPage: 100}} + protectRepositoryBranchesOptions = gitlab.ProtectRepositoryBranchesOptions{} + protectedBranchOrderBy []string + forceProtect bool + protectedBranchFormat = util.Dict{ + { + Key: "COUNT", + Value: "[%d]", + }, + { + Key: "REPOSITORY", + Value: "%s", + }, + { + Key: "BRANCHES", + Value: "%s", + }, + { + Key: "HOST", + Value: "[%s]", + }, + { + Key: "CACHED", + Value: "[%s]", + }, + } +) + +func NewProtectedBranchesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "protected", + Short: "Protected branches API", + } + + cmd.AddCommand( + NewProtectedBranchesListCmd(), + NewProtectRepositoryBranchesCmd(), + ) + + return cmd +} + +func NewProtectedBranchesListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List protected branches", + Long: "Gets a list of protected branches from a project as they are defined in the UI. If a wildcard is set, it is returned instead of the exact name of the branches that match that wildcard.", + RunE: func(cmd *cobra.Command, args []string) error { + return ProtectedBranchesListCmd() + }, + } + + cmd.Flags().Var(util.NewEnumValue(&sortBy, "asc", "desc"), "sort", + "Return protected branches sorted in asc or desc order. Default is desc") + + cmd.Flags().StringSliceVar(&protectedBranchOrderBy, "order_by", []string{"count", protectedBranchDefaultField}, + `Return protected branches ordered by web_url, created_at, title, updated_at or any nested field. Default is web_url.`) + + listProjectsOptionsFlags(cmd, &listProjectsOptions) + + return cmd +} + +func NewProtectRepositoryBranchesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "protect", + Short: "Protect repository branches", + Long: "Protects a single repository branch or several project repository branches using a wildcard protected branch.", + RunE: func(cmd *cobra.Command, args []string) error { + return ProtectRepositoryBranchesCmd() + }, + } + + cmd.Flags().BoolVar(&forceProtect, "force", false, + "Force update already protected branches.") + + cmd.Flags().Var(util.NewStringPtrValue(&protectRepositoryBranchesOptions.Name), "name", + "The name of the branch or wildcard") + + cmd.Flags().Var(util.NewAccessLevelValue(&protectRepositoryBranchesOptions.PushAccessLevel), "push_access_level", + "Access levels allowed to push (defaults: 40, Maintainer role)") + + cmd.Flags().Var(util.NewAccessLevelValue(&protectRepositoryBranchesOptions.MergeAccessLevel), "merge_access_level", + "Access levels allowed to merge (defaults: 40, Maintainer role)") + + cmd.Flags().Var(util.NewAccessLevelValue(&protectRepositoryBranchesOptions.UnprotectAccessLevel), "unprotect_access_level", + "Access levels allowed to unprotect (defaults: 40, Maintainer role)") + + cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowForcePush), "allow_force_push", + "Allow all users with push access to force push. (default: false)") + + // cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowedToPush), "allowed_to_push", + // "Array of access levels allowed to push, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}") + + // cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowedToMerge), "allowed_to_merge", + // "Array of access levels allowed to merge, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}") + + // cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.AllowedToUnprotect), "allowed_to_unprotect", + // "Array of access levels allowed to unprotect, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}") + + cmd.Flags().Var(util.NewBoolPtrValue(&protectRepositoryBranchesOptions.CodeOwnerApprovalRequired), "code_owner_approval_required", + "Prevent pushes to this branch if it matches an item in the CODEOWNERS file. (defaults: false)") + + cmd.MarkFlagRequired("name") + + listProjectsOptionsFlags(cmd, &listProjectsOptions) + + return cmd +} + +type ProjectProtectedBranch struct { + Project *gitlab.Project `json:"project,omitempty"` + ProtectedBranches []*gitlab.ProtectedBranch `json:"protected_branches,omitempty"` +} + +func (pb *ProjectProtectedBranch) BranchesNames() []string { + switch len(pb.ProtectedBranches) { + case 0: + return []string{} + case 1: + return []string{pb.ProtectedBranches[0].Name} + } + + branches := make([]string, 0, len(pb.ProtectedBranches)) + for _, b := range pb.ProtectedBranches { + branches = util.InsertString(branches, b.Name) + } + return branches +} + +func (pb *ProjectProtectedBranch) Search(name string) (*gitlab.ProtectedBranch, bool) { + switch len(pb.ProtectedBranches) { + case 0: + return nil, false + case 1: + return pb.ProtectedBranches[0], pb.ProtectedBranches[0].Name == name + } + // linear search + for _, b := range pb.ProtectedBranches { + if b.Name == name { + return b, true + } + } + return nil, false +} + +func ProtectedBranchesListCmd() error { + if !sort.ValidOrderBy(protectedBranchOrderBy, ProjectProtectedBranch{}) { + protectedBranchOrderBy = append(protectedBranchOrderBy, protectedBranchDefaultField) + } + + wg := common.Limiter + data := make(chan interface{}) + defer func() { + for _, err := range wg.Errors() { + hclog.L().Error(err.Err.Error()) + } + }() + + for _, h := range common.Client.Hosts { + fmt.Printf("Getting protected branches from %s ...\n", h.URL) + wg.Add(1) + go listProjects(h, listProjectsOptions, wg, data, common.Client.WithCache()) + } + + go func() { + wg.Wait() + close(data) + }() + + toList := make(sort.Elements, 0) + for e := range data { + toList = append(toList, e) + } + + if len(toList) == 0 { + return fmt.Errorf("no projects found") + } + + protectedBranches := make(chan interface{}) + for _, v := range toList.Typed() { + wg.Add(1) + go listProtectedBranches(v.Host, v.Struct.(*gitlab.Project), listProtectedBranchesOptions, wg, protectedBranches, common.Client.WithCache()) + } + + go func() { + wg.Wait() + close(protectedBranches) + }() + + results, err := sort.FromChannel(protectedBranches, &sort.Options{ + OrderBy: protectedBranchOrderBy, + SortBy: sortBy, + GroupBy: protectedBranchDefaultField, + StructType: ProjectProtectedBranch{}, + }) + if err != nil { + return err + } + + if len(results) == 0 { + return fmt.Errorf("no protected branches found") + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) + if _, err := fmt.Fprintln(w, strings.Join(protectedBranchFormat.Keys(), "\t")); err != nil { + return err + } + + unique := 0 + total := 0 + + for _, r := range results { + unique++ + total += r.Count + for _, v := range r.Elements.Typed() { + if err := protectedBranchFormat.Print(w, "\t", + r.Count, + r.Key, + v.Struct.(*ProjectProtectedBranch).BranchesNames(), + v.Host.ProjectName(), + r.Cached, + ); err != nil { + return err + } + } + + } + + if err := totalFormat.Print(w, "\n", unique, total, len(wg.Errors())); err != nil { + return err + } + + if err := w.Flush(); err != nil { + return err + } + + return nil +} + +func ProtectRepositoryBranchesCmd() error { + if !sort.ValidOrderBy(protectedBranchOrderBy, ProjectProtectedBranch{}) { + protectedBranchOrderBy = append(protectedBranchOrderBy, protectedBranchDefaultField) + } + + wg := common.Limiter + data := make(chan interface{}) + defer func() { + for _, err := range wg.Errors() { + hclog.L().Error(err.Err.Error()) + } + }() + + for _, h := range common.Client.Hosts { + fmt.Printf("Getting protected branches from %s ...\n", h.URL) + wg.Add(1) + go listProjects(h, listProjectsOptions, wg, data, common.Client.WithCache()) + } + + go func() { + wg.Wait() + close(data) + }() + + toList := make(sort.Elements, 0) + for e := range data { + toList = append(toList, e) + } + + if len(toList) == 0 { + return fmt.Errorf("no projects found") + } + + protectedBranches := make(chan interface{}) + for _, v := range toList.Typed() { + wg.Add(1) + go listProtectedBranches(v.Host, v.Struct.(*gitlab.Project), listProtectedBranchesOptions, wg, protectedBranches, common.Client.WithCache()) + } + + go func() { + wg.Wait() + close(protectedBranches) + }() + + toProtect := make(sort.Elements, 0) + for e := range protectedBranches { + if v := e.(sort.Element).Struct.(*ProjectProtectedBranch); forceProtect || len(v.ProtectedBranches) == 0 { + toProtect = append(toProtect, e) + } + } + + if len(toProtect) == 0 { + return fmt.Errorf("branch %q is already protected in %d repositories in %v", + *protectRepositoryBranchesOptions.Name, len(toList), common.Client.Hosts.Projects(common.Config.ShowAll)) + } + + util.AskUser(fmt.Sprintf("Do you really want to protect branch %q in %d repositories in %v ?", + *protectRepositoryBranchesOptions.Name, len(toProtect), common.Client.Hosts.Projects(common.Config.ShowAll))) + + protectedCh := make(chan interface{}) + for _, v := range toProtect.Typed() { + wg.Add(1) + go protectRepositoryBranches(v.Host, v.Struct.(*ProjectProtectedBranch), forceProtect, protectRepositoryBranchesOptions, wg, protectedCh, common.Client.WithNoCache()) + } + + go func() { + wg.Wait() + close(protectedCh) + }() + + results, err := sort.FromChannel(protectedCh, &sort.Options{ + OrderBy: protectedBranchOrderBy, + SortBy: sortBy, + GroupBy: protectedBranchDefaultField, + StructType: ProjectProtectedBranch{}, + }) + if err != nil { + return err + } + + if len(results) == 0 { + return fmt.Errorf("no protected branches found") + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) + if _, err := fmt.Fprintln(w, strings.Join(protectedBranchFormat.Keys(), "\t")); err != nil { + return err + } + + unique := 0 + total := 0 + + for _, r := range results { + unique++ + total += r.Count + for _, v := range r.Elements.Typed() { + if err := protectedBranchFormat.Print(w, "\t", + r.Count, + r.Key, + v.Struct.(*ProjectProtectedBranch).BranchesNames(), + v.Host.ProjectName(), + r.Cached, + ); err != nil { + return err + } + } + + } + + if err := totalFormat.Print(w, "\n", unique, total, len(wg.Errors())); err != nil { + return err + } + + if err := w.Flush(); err != nil { + return err + } + + return nil +} + +func listProtectedBranches(h *client.Host, project *gitlab.Project, opt gitlab.ListProtectedBranchesOptions, + wg *limiter.Limiter, data chan<- interface{}, options ...gitlab.RequestOptionFunc) error { + + defer wg.Done() + + wg.Lock() + list, resp, err := h.Client.ProtectedBranches.ListProtectedBranches(project.ID, &opt, options...) + wg.Unlock() + if err != nil { + wg.Error(h, err) + return err + } + + data <- sort.Element{ + Host: h, + Struct: &ProjectProtectedBranch{ + Project: project, + ProtectedBranches: list}, + Cached: resp.Header.Get("X-From-Cache") == "1"} + + if resp.NextPage > 0 { + wg.Add(1) + opt.Page = resp.NextPage + go listProtectedBranches(h, project, opt, wg, data, options...) + } + + return nil +} + +func protectRepositoryBranches(h *client.Host, pb *ProjectProtectedBranch, forceProtect bool, opt gitlab.ProtectRepositoryBranchesOptions, + wg *limiter.Limiter, data chan<- interface{}, options ...gitlab.RequestOptionFunc) error { + + defer wg.Done() + + if forceProtect { + if old, ok := pb.Search(*opt.Name); ok { + new := opt + + new.AllowForcePush = &old.AllowForcePush + new.CodeOwnerApprovalRequired = &old.CodeOwnerApprovalRequired + + switch n := len(old.MergeAccessLevels); n { + case 0: + case 1: + new.MergeAccessLevel = &old.MergeAccessLevels[0].AccessLevel + default: + allowedToMerge := make([]*gitlab.BranchPermissionOptions, 0, n) + for _, l := range old.MergeAccessLevels { + allowedToMerge = append(allowedToMerge, &gitlab.BranchPermissionOptions{ + UserID: &l.UserID, + GroupID: &l.GroupID, + AccessLevel: &l.AccessLevel, + }) + } + new.AllowedToMerge = &allowedToMerge + } + + switch n := len(old.PushAccessLevels); n { + case 0: + case 1: + new.PushAccessLevel = &old.PushAccessLevels[0].AccessLevel + default: + allowedToPush := make([]*gitlab.BranchPermissionOptions, 0, n) + for _, l := range old.PushAccessLevels { + allowedToPush = append(allowedToPush, &gitlab.BranchPermissionOptions{ + UserID: &l.UserID, + GroupID: &l.GroupID, + AccessLevel: &l.AccessLevel, + }) + } + new.AllowedToPush = &allowedToPush + } + + switch n := len(old.UnprotectAccessLevels); n { + case 0: + case 1: + new.UnprotectAccessLevel = &old.UnprotectAccessLevels[0].AccessLevel + default: + allowedToUnprotect := make([]*gitlab.BranchPermissionOptions, 0, n) + for _, l := range old.UnprotectAccessLevels { + allowedToUnprotect = append(allowedToUnprotect, &gitlab.BranchPermissionOptions{ + UserID: &l.UserID, + GroupID: &l.GroupID, + AccessLevel: &l.AccessLevel, + }) + } + new.AllowedToUnprotect = &allowedToUnprotect + } + + if err := mergo.Merge(&new, opt, mergo.WithOverwriteWithEmptyValue); err != nil { + wg.Error(h, err) + return err + } + + wg.Lock() + _, err := h.Client.ProtectedBranches.UnprotectRepositoryBranches(pb.Project.ID, *new.Name, options...) + wg.Unlock() + if err != nil { + wg.Error(h, err) + return err + } + + opt = new + } + } + + wg.Lock() + v, resp, err := h.Client.ProtectedBranches.ProtectRepositoryBranches(pb.Project.ID, &opt, options...) + wg.Unlock() + if err != nil { + wg.Error(h, err) + return err + } + + data <- sort.Element{ + Host: h, + Struct: &ProjectProtectedBranch{ + Project: pb.Project, + ProtectedBranches: []*gitlab.ProtectedBranch{v}}, + Cached: resp.Header.Get("X-From-Cache") == "1"} + + return nil +}