gabor-boros/minutes

View on GitHub
cmd/root/cmd.go

Summary

Maintainability
A
0 mins
Test Coverage
package root

import (
    "context"
    "fmt"
    "os"
    "regexp"
    "strings"
    "time"

    "github.com/gabor-boros/minutes/internal/cmd/utils"

    "github.com/jedib0t/go-pretty/v6/progress"
    "github.com/jedib0t/go-pretty/v6/table"

    "github.com/gabor-boros/minutes/internal/pkg/client"
    "github.com/gabor-boros/minutes/internal/pkg/worklog"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

const (
    program           string = "minutes"
    defaultDateFormat string = "2006-01-02 15:04:05"
)

var (
    configFile string
    envPrefix  string

    version string
    commit  string
    date    string

    rootCmd = &cobra.Command{
        Use:   program,
        Short: "Sync worklogs between multiple time trackers, invoicing, and bookkeeping software.",
        Long: `
Minutes is a CLI tool for synchronizing work logs between multiple time
trackers, invoicing, and bookkeeping software to make entrepreneurs'
daily work easier.

Every source and destination comes with their specific flags. Before using any
flags, check the related documentation.

Minutes comes with absolutely NO WARRANTY; for more information, visit the
project's home page.

Project home page: https://gabor-boros.github.io/minutes
Report bugs at: https://github.com/gabor-boros/minutes/issues
Report security issues to: gabor.brs@gmail.com`,
        Run: runRootCmd,
    }
)

func init() {
    envPrefix = strings.ToUpper(program)

    cobra.OnInitialize(initConfig)

    initCommonFlags()
    initClockifyFlags()
    initHarvestFlags()
    initTempoFlags()
    initTimewarriorFlags()
    initTogglFlags()
}

func initConfig() {
    if configFile != "" {
        viper.SetConfigName(configFile)
    } else {
        homeDir, err := os.UserHomeDir()
        cobra.CheckErr(err)

        configDir, err := os.UserConfigDir()
        cobra.CheckErr(err)

        viper.AddConfigPath(homeDir)
        viper.AddConfigPath(configDir)
        viper.SetConfigName("." + program)
        viper.SetConfigType("toml")
    }

    viper.SetEnvPrefix(envPrefix)
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            cobra.CheckErr(err)
        }
    } else {
        fmt.Println("Using config file:", viper.ConfigFileUsed(), configFile)
    }

    // Bind flags to config value
    cobra.CheckErr(viper.BindPFlags(rootCmd.Flags()))
}

func runRootCmd(_ *cobra.Command, _ []string) {
    var err error

    if viper.GetBool("version") {
        if version == "" || len(commit) < 7 || date == "" {
            fmt.Println("dirty build")
        } else {
            fmt.Printf("%s version %s, commit %s (%s)\n", program, version, commit[:7], date)
        }
        os.Exit(0)
    }

    validateFlags()

    dateFormat := viper.GetString("date-format")

    start, err := utils.GetTime(viper.GetString("start"), dateFormat)
    cobra.CheckErr(err)

    rawEnd := viper.GetString("end")
    end, err := utils.GetTime(rawEnd, dateFormat)
    cobra.CheckErr(err)

    // No end date was set, hence we are setting the end date to next day midnight
    if rawEnd == "" {
        end = end.Add(time.Hour * 24)
    }

    fetcher, err := getFetcher()
    cobra.CheckErr(err)

    uploader, err := getUploader()
    cobra.CheckErr(err)

    tagsAsTasksRegex, err := regexp.Compile(viper.GetString("tags-as-tasks-regex"))
    cobra.CheckErr(err)

    entries, err := fetcher.FetchEntries(context.Background(), &client.FetchOpts{
        End:              end,
        Start:            start,
        User:             viper.GetString("source-user"),
        TagsAsTasksRegex: tagsAsTasksRegex,
    })
    cobra.CheckErr(err)

    // It is safe to use MustCompile when compiling regex as we already
    // validated its correctness
    wl := worklog.NewWorklog(entries, &worklog.FilterOpts{
        Client:  regexp.MustCompile(viper.GetString("filter-client")),
        Project: regexp.MustCompile(viper.GetString("filter-project")),
    })

    completeEntries := wl.CompleteEntries()
    incompleteEntries := wl.IncompleteEntries()

    columnTruncates := map[string]int{}
    err = viper.UnmarshalKey("table-column-truncates", &columnTruncates)
    cobra.CheckErr(err)

    tablePrinter := utils.NewTablePrinter(&utils.TablePrinterOpts{
        BasePrinterOpts: utils.BasePrinterOpts{
            Output:        os.Stdout,
            AutoIndex:     true,
            Title:         fmt.Sprintf("Worklog entries (%s - %s)", start.Local().String(), end.Local().String()),
            SortBy:        viper.GetStringSlice("table-sort-by"),
            HiddenColumns: viper.GetStringSlice("table-hide-column"),
        },
        Style: table.StyleLight,
        ColumnConfig: utils.ParseColumnConfigs(
            "table-column-config.%s",
            viper.GetStringSlice("table-hide-column"),
        ),
        ColumnTruncates: columnTruncates,
    })

    err = tablePrinter.Print(completeEntries, incompleteEntries)
    cobra.CheckErr(err)

    if strings.ToLower(utils.Prompt("Continue? [y/n]: ")) != "y" {
        fmt.Println("User interruption. Aborting.")
        os.Exit(0)
    }

    // In worst case, the maximum number of errors will match the number of entries
    uploadErrChan := make(chan error, len(completeEntries))

    fmt.Printf("\nUploading worklog entries:\n\n")
    if !viper.GetBool("dry-run") {
        progressUpdateFrequency := progress.DefaultUpdateFrequency
        progressWriter := utils.NewProgressWriter(progressUpdateFrequency)

        // Intentionally called as a goroutine
        go progressWriter.Render()

        uploader.UploadEntries(context.Background(), completeEntries, uploadErrChan, &client.UploadOpts{
            RoundToClosestMinute:   viper.GetBool("round-to-closest-minute"),
            TreatDurationAsBilled:  viper.GetBool("force-billed-duration"),
            CreateMissingResources: false,
            User:                   viper.GetString("target-user"),
            ProgressWriter:         progressWriter,
        })

        // Wait for at least one tracker to appear and while the rendering is in progress,
        // wait for the remaining updates to render.
        time.Sleep(time.Second)
        for progressWriter.IsRenderInProgress() {
            time.Sleep(progressUpdateFrequency)
        }
    }

    var uploadErrors []error
    for i := 0; i < len(completeEntries); i++ {
        if err := <-uploadErrChan; err != nil {
            uploadErrors = append(uploadErrors, err)
        }
    }

    if errCount := len(uploadErrors); errCount != 0 {
        fmt.Printf("\nFailed to upload %d worklog entries!\n\n", errCount)
        for _, err := range uploadErrors {
            fmt.Println(err)
        }
        os.Exit(1)
    }

    fmt.Printf("\nSuccessfully uploaded %d worklog entries!\n", len(completeEntries))
}

func Execute(buildVersion string, buildCommit string, buildDate string) {
    version = buildVersion
    commit = buildCommit
    date = buildDate

    cobra.CheckErr(rootCmd.Execute())
}