diff --git a/cmd/offboard/offboard.go b/cmd/offboard/offboard.go new file mode 100644 index 0000000..feabfd8 --- /dev/null +++ b/cmd/offboard/offboard.go @@ -0,0 +1,139 @@ +package offboard + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/spf13/cobra" + + "github.com/open-sauced/pizza-cli/v2/pkg/config" + "github.com/open-sauced/pizza-cli/v2/pkg/constants" + "github.com/open-sauced/pizza-cli/v2/pkg/utils" +) + +type Options struct { + offboardingUsers []string + + // config file path + configPath string + + // repository path + path string + + // from global config + ttyDisabled bool + + // telemetry for capturing CLI events via PostHog + telemetry *utils.PosthogCliClient +} + +const offboardLongDesc string = `CAUTION: Experimental Command. Removes users from the \".sauced.yaml\" config and \"CODEOWNERS\" files. +Requires the users' name OR email.` + +func NewConfigCommand() *cobra.Command { + opts := &Options{} + cmd := &cobra.Command{ + Use: "offboard [flags]", + Short: "CAUTION: Experimental Command. Removes users from the \".sauced.yaml\" config and \"CODEOWNERS\" files.", + Long: offboardLongDesc, + Args: func(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("you must provide at least one argument: the offboarding user's email/username") + } + + opts.offboardingUsers = args + + return nil + }, + RunE: func(cmd *cobra.Command, _ []string) error { + opts.ttyDisabled, _ = cmd.Flags().GetBool("tty-disable") + opts.configPath, _ = cmd.Flags().GetString("config") + disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) + + opts.telemetry = utils.NewPosthogCliClient(!disableTelem) + + opts.path, _ = cmd.Flags().GetString("path") + err := run(opts) + _ = opts.telemetry.Done() + + return err + }, + } + + cmd.PersistentFlags().StringP("path", "p", "", "the path to the repository (required)") + if err := cmd.MarkPersistentFlagRequired("path"); err != nil { + fmt.Printf("error MarkPersistentFlagRequired: %v", err) + } + return cmd +} + +func run(opts *Options) error { + var spec *config.Spec + var err error + if len(opts.configPath) != 0 { + spec, _, err = config.LoadConfig(opts.configPath) + } else { + var configPath string + if strings.Compare(string(opts.path[len(opts.path)-1]), "/") == 0 { + configPath = opts.path + ".sauced.yaml" + } else { + configPath = opts.path + "/.sauced.yaml" + } + spec, _, err = config.LoadConfig(configPath) + } + + if err != nil { + _ = opts.telemetry.CaptureFailedOffboard() + return fmt.Errorf("error loading config: %v", err) + } + + var offboardingNames []string + attributions := spec.Attributions + for _, user := range opts.offboardingUsers { + added := false + + // deletes if the user is a name (key) + delete(attributions, user) + + // delete if the user is an email (value) + for k, v := range attributions { + if slices.Contains(v, user) { + offboardingNames = append(offboardingNames, k) + delete(attributions, k) + added = true + } + } + + if !added { + offboardingNames = append(offboardingNames, user) + } + } + + if len(opts.configPath) != 0 { + err = generateConfigFile(opts.configPath, attributions) + } else { + var configPath string + if strings.Compare(string(opts.path[len(opts.path)-1]), "/") == 0 { + configPath = opts.path + ".sauced.yaml" + } else { + configPath = opts.path + "/.sauced.yaml" + } + err = generateConfigFile(configPath, attributions) + } + + if err != nil { + _ = opts.telemetry.CaptureFailedOffboard() + return fmt.Errorf("error generating config file: %v", err) + } + + err = generateOwnersFile(opts.path, offboardingNames) + if err != nil { + _ = opts.telemetry.CaptureFailedOffboard() + return fmt.Errorf("error generating owners file: %v", err) + } + + _ = opts.telemetry.CaptureOffboard() + return nil +} diff --git a/cmd/offboard/output.go b/cmd/offboard/output.go new file mode 100644 index 0000000..f429fa0 --- /dev/null +++ b/cmd/offboard/output.go @@ -0,0 +1,87 @@ +package offboard + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/open-sauced/pizza-cli/v2/pkg/config" + "github.com/open-sauced/pizza-cli/v2/pkg/utils" +) + +func generateConfigFile(outputPath string, attributionMap map[string][]string) error { + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("error creating %s file: %w", outputPath, err) + } + defer file.Close() + + var config config.Spec + config.Attributions = attributionMap + + // for pretty print test + yaml, err := utils.OutputYAML(config) + + if err != nil { + return fmt.Errorf("failed to turn into YAML: %w", err) + } + + _, err = file.WriteString(yaml) + + if err != nil { + return fmt.Errorf("failed to turn into YAML: %w", err) + } + + return nil +} + +func generateOwnersFile(path string, offboardingUsers []string) error { + outputType := "/CODEOWNERS" + var owners []byte + var err error + + var ownersPath string + + if _, err = os.Stat(filepath.Join(path, "/CODEOWNERS")); !errors.Is(err, os.ErrNotExist) { + outputType = "CODEOWNERS" + ownersPath = filepath.Join(path, "/CODEOWNERS") + owners, err = os.ReadFile(ownersPath) + } else if _, err = os.Stat(filepath.Join(path, "OWNERS")); !errors.Is(err, os.ErrNotExist) { + outputType = "OWNERS" + ownersPath = filepath.Join(path, "/OWNERS") + owners, err = os.ReadFile(ownersPath) + } + + if err != nil { + fmt.Printf("will create a new %s file in the path %s", outputType, path) + } + + lines := strings.Split(string(owners), "\n") + var newLines []string + for _, line := range lines { + newLine := line + for _, name := range offboardingUsers { + result, _, found := strings.Cut(newLine, "@"+name) + if found { + newLine = result + } + } + newLines = append(newLines, newLine) + } + + output := strings.Join(newLines, "\n") + file, err := os.Create(ownersPath) + if err != nil { + return fmt.Errorf("error creating %s file: %w", outputType, err) + } + defer file.Close() + + _, err = file.WriteString(output) + if err != nil { + return fmt.Errorf("failed writing file %s: %w", path+outputType, err) + } + + return nil +} diff --git a/cmd/root/root.go b/cmd/root/root.go index d41a435..0cd054f 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -10,6 +10,7 @@ import ( "github.com/open-sauced/pizza-cli/v2/cmd/docs" "github.com/open-sauced/pizza-cli/v2/cmd/generate" "github.com/open-sauced/pizza-cli/v2/cmd/insights" + "github.com/open-sauced/pizza-cli/v2/cmd/offboard" "github.com/open-sauced/pizza-cli/v2/cmd/version" "github.com/open-sauced/pizza-cli/v2/pkg/constants" ) @@ -44,6 +45,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd.AddCommand(generate.NewGenerateCommand()) cmd.AddCommand(insights.NewInsightsCommand()) cmd.AddCommand(version.NewVersionCommand()) + cmd.AddCommand(offboard.NewConfigCommand()) // The docs command is hidden as it's only used by the pizza-cli maintainers docsCmd := docs.NewDocsCommand() diff --git a/pkg/utils/posthog.go b/pkg/utils/posthog.go index 9a9fb41..10d66d1 100644 --- a/pkg/utils/posthog.go +++ b/pkg/utils/posthog.go @@ -198,6 +198,28 @@ func (p *PosthogCliClient) CaptureFailedConfigGenerate() error { return nil } +func (p *PosthogCliClient) CaptureOffboard() error { + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_offboard", + }) + } + + return nil +} + +func (p *PosthogCliClient) CaptureFailedOffboard() error { + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_failed_to_offboard", + }) + } + + return nil +} + // CaptureInsights gathers telemetry on successful Insights command runs func (p *PosthogCliClient) CaptureInsights() error { if p.activated {