github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/label/clone.go (about) 1 package label 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "sync/atomic" 9 10 "github.com/MakeNowJust/heredoc" 11 "github.com/ungtb10d/cli/v2/internal/ghrepo" 12 "github.com/ungtb10d/cli/v2/internal/text" 13 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 14 "github.com/ungtb10d/cli/v2/pkg/iostreams" 15 "github.com/spf13/cobra" 16 "golang.org/x/sync/errgroup" 17 ) 18 19 type cloneOptions struct { 20 BaseRepo func() (ghrepo.Interface, error) 21 HttpClient func() (*http.Client, error) 22 IO *iostreams.IOStreams 23 24 SourceRepo ghrepo.Interface 25 Force bool 26 } 27 28 func newCmdClone(f *cmdutil.Factory, runF func(*cloneOptions) error) *cobra.Command { 29 opts := cloneOptions{ 30 HttpClient: f.HttpClient, 31 IO: f.IOStreams, 32 } 33 34 cmd := &cobra.Command{ 35 Use: "clone <source-repository>", 36 Short: "Clones labels from one repository to another", 37 Long: heredoc.Doc(` 38 Clones labels from a source repository to a destination repository on GitHub. 39 By default, the destination repository is the current repository. 40 41 All labels from the source repository will be copied to the destination 42 repository. Labels in the destination repository that are not in the source 43 repository will not be deleted or modified. 44 45 Labels from the source repository that already exist in the destination 46 repository will be skipped. You can overwrite existing labels in the 47 destination repository using the --force flag. 48 `), 49 Example: heredoc.Doc(` 50 # clone and overwrite labels from ungtb10d/cli repository into the current repository 51 $ gh label clone ungtb10d/cli --force 52 53 # clone labels from ungtb10d/cli repository into a octocat/cli repository 54 $ gh label clone ungtb10d/cli --repo octocat/cli 55 `), 56 Args: cmdutil.ExactArgs(1, "cannot clone labels: source-repository argument required"), 57 RunE: func(c *cobra.Command, args []string) error { 58 var err error 59 opts.BaseRepo = f.BaseRepo 60 opts.SourceRepo, err = ghrepo.FromFullName(args[0]) 61 if err != nil { 62 return err 63 } 64 if runF != nil { 65 return runF(&opts) 66 } 67 return cloneRun(&opts) 68 }, 69 } 70 71 cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Overwrite labels in the destination repository") 72 73 return cmd 74 } 75 76 func cloneRun(opts *cloneOptions) error { 77 httpClient, err := opts.HttpClient() 78 if err != nil { 79 return err 80 } 81 82 baseRepo, err := opts.BaseRepo() 83 if err != nil { 84 return err 85 } 86 87 opts.IO.StartProgressIndicator() 88 successCount, totalCount, err := cloneLabels(httpClient, baseRepo, opts) 89 opts.IO.StopProgressIndicator() 90 if err != nil { 91 return err 92 } 93 94 if opts.IO.IsStdoutTTY() { 95 cs := opts.IO.ColorScheme() 96 pluralize := func(num int) string { 97 return text.Pluralize(num, "label") 98 } 99 100 successCount := int(successCount) 101 switch { 102 case successCount == totalCount: 103 fmt.Fprintf(opts.IO.Out, "%s Cloned %s from %s to %s\n", cs.SuccessIcon(), pluralize(successCount), ghrepo.FullName(opts.SourceRepo), ghrepo.FullName(baseRepo)) 104 default: 105 fmt.Fprintf(opts.IO.Out, "%s Cloned %s of %d from %s to %s\n", cs.WarningIcon(), pluralize(successCount), totalCount, ghrepo.FullName(opts.SourceRepo), ghrepo.FullName(baseRepo)) 106 } 107 } 108 109 return nil 110 } 111 112 func cloneLabels(client *http.Client, destination ghrepo.Interface, opts *cloneOptions) (successCount uint32, totalCount int, err error) { 113 successCount = 0 114 labels, totalCount, err := listLabels(client, opts.SourceRepo, listQueryOptions{Limit: -1}) 115 if err != nil { 116 return 117 } 118 119 workers := 10 120 toCreate := make(chan createOptions) 121 122 wg, ctx := errgroup.WithContext(context.Background()) 123 for i := 0; i < workers; i++ { 124 wg.Go(func() error { 125 for { 126 select { 127 case <-ctx.Done(): 128 return nil 129 case l, ok := <-toCreate: 130 if !ok { 131 return nil 132 } 133 err := createLabel(client, destination, &l) 134 if err != nil { 135 if !errors.Is(err, errLabelAlreadyExists) { 136 return err 137 } 138 } else { 139 atomic.AddUint32(&successCount, 1) 140 } 141 } 142 } 143 }) 144 } 145 146 for _, label := range labels { 147 createOpts := createOptions{ 148 Name: label.Name, 149 Description: label.Description, 150 Color: label.Color, 151 Force: opts.Force, 152 } 153 toCreate <- createOpts 154 } 155 156 close(toCreate) 157 err = wg.Wait() 158 159 return 160 }