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  }