diff --git a/cmd/root.go b/cmd/root.go index aa71efe..eccf5dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,21 +10,22 @@ import ( "os" ) -var cfgFile string +var conf internal.Configuration func init() { cobra.OnInitialize(func() { - if cfgFile != "" { + conf, _ := rootCmd.PersistentFlags().GetString(internal.FlagConfiguration) + if conf != "" { // Use Configuration file from the flag. - viper.SetConfigFile(cfgFile) + viper.SetConfigFile(conf) } }) - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "specify the configuration file") - rootCmd.PersistentFlags().BoolVarP(&internal.Config.Insecure, internal.FlagInsecure, "k", false, "use http instead of https") - rootCmd.PersistentFlags().StringVarP(&internal.Config.Host, internal.FlagHost, "H", "", "specify the host to use for effort query") - rootCmd.PersistentFlags().StringVarP(&internal.Config.Username, internal.FlagUsername, "u", "", "specify the username to use for server authentication") - rootCmd.PersistentFlags().StringVarP(&internal.Config.Password, internal.FlagPassword, "p", "", "specify the password to use for server authentication") + rootCmd.PersistentFlags().StringP(internal.FlagConfiguration, "c", "", "specify the configuration file") + rootCmd.PersistentFlags().BoolVarP(&conf.Http, internal.FlagHttp, "k", false, "use http instead of https") + rootCmd.PersistentFlags().StringVarP(&conf.Host, internal.FlagHost, "H", "", "specify the host to use for effort query") + rootCmd.PersistentFlags().StringVarP(&conf.Username, internal.FlagUsername, "u", "", "specify the username to use for server authentication") + rootCmd.PersistentFlags().StringVarP(&conf.Password, internal.FlagPassword, "p", "", "specify the password to use for server authentication") rootCmd.MarkPersistentFlagRequired(internal.FlagHost) } @@ -34,14 +35,14 @@ var rootCmd = &cobra.Command{ Args: cobra.NoArgs, Version: internal.Version, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if cfgFile != "" { + if viper.ConfigFileUsed() != "" { viper.BindPFlags(cmd.Flags()) if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - return fmt.Errorf("can't read configuration. %s", err.Error()) + return fmt.Errorf("can't read conf. %s", err.Error()) } } - viper.Unmarshal(&internal.Config) + viper.Unmarshal(&conf) } // Remove required annotation if the user has that flag given with viper. cmd.Flags().VisitAll(func(flag *pflag.Flag) { diff --git a/cmd/show.go b/cmd/show.go index fb6f5ea..52438c3 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -15,18 +15,19 @@ func init() { rootCmd.AddCommand(showCmd) showCmd.AddCommand(showBcsCmd, showJiraCmd) - showCmd.PersistentFlags().IntVar(&internal.Config.Year, internal.FlagYear, time.Now().Year(), "specify the year to query effort for") - showCmd.PersistentFlags().IntVar(&internal.Config.Month, internal.FlagMonth, int(time.Now().Month()), "specify the month to query effort for") - showCmd.PersistentFlags().BoolVarP(&internal.Config.Summarize, internal.FlagSummarize, "s", false, "summarize effort per day") - showCmd.PersistentFlags().BoolVar(&internal.Config.PrintEmptyLine, internal.FlagPrintEmptyLine, false, "print empty line for missing day during summary") - showCmd.PersistentFlags().BoolVar(&internal.Config.Seconds, internal.FlagSeconds, false, "display duration in seconds") + showCmd.PersistentFlags().IntVar(&conf.Year, internal.FlagYear, time.Now().Year(), "specify the year to query effort for") + showCmd.PersistentFlags().IntVar(&conf.Month, internal.FlagMonth, int(time.Now().Month()), "specify the month to query effort for") + showCmd.PersistentFlags().BoolVarP(&conf.Duration.Summarize, internal.FlagSummarize, "s", false, "summarize effort per day") + showCmd.PersistentFlags().BoolVar(&conf.Duration.Empty, internal.FlagEmpty, false, "print empty durations during summary") + showCmd.PersistentFlags().BoolVar(&conf.Duration.Decimal, internal.FlagDecimal, false, "display duration as decimal hour") + showCmd.PersistentFlags().BoolVar(&conf.Duration.Negate, internal.FlagNegate, false, "negate decimal duration") - showBcsCmd.Flags().StringVar(&internal.Config.Report, internal.FlagReport, "", "specify the name of the report") - showBcsCmd.Flags().StringArrayVar(&internal.Config.Projects, internal.FlagProjects, nil, "specify the oid of the project") + showBcsCmd.Flags().StringVar(&conf.Report, internal.FlagReport, "", "specify the name of the report") + showBcsCmd.Flags().StringArrayVar(&conf.Projects, internal.FlagProjects, nil, "specify the oid of the project") showBcsCmd.MarkFlagRequired(internal.FlagReport) - showJiraCmd.Flags().StringArrayVar(&internal.Config.Projects, internal.FlagProjects, nil, "specify the project key") - showJiraCmd.Flags().StringArrayVar(&internal.Config.Users, internal.FlagUsers, nil, "show results for user") + showJiraCmd.Flags().StringArrayVar(&conf.Projects, internal.FlagProjects, nil, "specify the project key") + showJiraCmd.Flags().StringArrayVar(&conf.Users, internal.FlagUsers, nil, "show results for user") } var showCmd = &cobra.Command{ @@ -40,8 +41,11 @@ var showCmd = &cobra.Command{ if err != nil { return err } - if !internal.Config.Summarize && internal.Config.PrintEmptyLine { - return fmt.Errorf("empty lines (--%s) are only available for summaries (--%s)", internal.FlagPrintEmptyLine, internal.FlagSummarize) + if !conf.Duration.Summarize && conf.Duration.Empty { + return fmt.Errorf("empty durations (--%s) are only available for summaries (--%s)", internal.FlagEmpty, internal.FlagSummarize) + } + if !conf.Duration.Decimal && conf.Duration.Negate { + return fmt.Errorf("negative durations (--%s) are only available for decimal values (--%s)", internal.FlagNegate, internal.FlagDecimal) } return nil }, @@ -57,31 +61,31 @@ var showBcsCmd = &cobra.Command{ if err != nil { return err } - if internal.Config.Projects != nil && len(internal.Config.Projects) > 1 { + if conf.Projects != nil && len(conf.Projects) > 1 { return fmt.Errorf("only one project allowed") } return nil }, Run: func(cmd *cobra.Command, args []string) { - if internal.Config.Projects == nil || len(internal.Config.Projects) == 0 { + if conf.Projects == nil || len(conf.Projects) == 0 { bcs.GetTimesheet( pkg.NewHttpClient(), - internal.Config.Server(), - internal.Config.Userinfo(), - internal.Config.Year, - time.Month(internal.Config.Month), - internal.Config.Report, - ).Print(os.Stdout, false, internal.Config.Summarize, internal.Config.PrintEmptyLine, internal.Config.Seconds) + conf.Server(), + conf.Userinfo(), + conf.Year, + time.Month(conf.Month), + conf.Report, + ).Print(os.Stdout, false, &conf.Duration) } else { bcs.GetBulkTimesheet( pkg.NewHttpClient(), - internal.Config.Server(), - internal.Config.Userinfo(), - internal.Config.Year, - time.Month(internal.Config.Month), - pkg.Project(internal.Config.Projects[0]), - internal.Config.Report, - ).Print(os.Stdout, true, internal.Config.Summarize, internal.Config.PrintEmptyLine, internal.Config.Seconds) + conf.Server(), + conf.Userinfo(), + conf.Year, + time.Month(conf.Month), + pkg.Project(conf.Projects[0]), + conf.Report, + ).Print(os.Stdout, true, &conf.Duration) } }, } @@ -92,25 +96,25 @@ var showJiraCmd = &cobra.Command{ Long: "Show your worklog data from Atlassian Jira.", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - if internal.Config.Users == nil || len(internal.Config.Users) == 0 { + if conf.Users == nil || len(conf.Users) == 0 { jira.GetTimesheet( pkg.NewHttpClient(), - internal.Config.Server(), - internal.Config.Userinfo(), - internal.Config.Year, - time.Month(internal.Config.Month), - internal.Projects(internal.Config.Projects), - ).Print(os.Stdout, false, internal.Config.Summarize, internal.Config.PrintEmptyLine, internal.Config.Seconds) + conf.Server(), + conf.Userinfo(), + conf.Year, + time.Month(conf.Month), + pkg.Projects(conf.Projects), + ).Print(os.Stdout, false, &conf.Duration) } else { jira.GetBulkTimesheet( pkg.NewHttpClient(), - internal.Config.Server(), - internal.Config.Userinfo(), - internal.Config.Year, - time.Month(internal.Config.Month), - internal.Projects(internal.Config.Projects), - internal.Users(internal.Config.Users), - ).Print(os.Stdout, true, internal.Config.Summarize, internal.Config.PrintEmptyLine, internal.Config.Seconds) + conf.Server(), + conf.Userinfo(), + conf.Year, + time.Month(conf.Month), + pkg.Projects(conf.Projects), + pkg.Users(conf.Users), + ).Print(os.Stdout, true, &conf.Duration) } }, } diff --git a/internal/config.go b/internal/config.go deleted file mode 100644 index b088486..0000000 --- a/internal/config.go +++ /dev/null @@ -1,84 +0,0 @@ -package internal - -import ( - "eager/pkg" - "net/url" -) - -const ( - Version = "0.1.0" - FlagInsecure = "insecure" - FlagHost = "host" - FlagUsername = "username" - FlagPassword = "password" - FlagYear = "year" - FlagMonth = "month" - FlagProjects = "project" - FlagUsers = "user" - FlagReport = "report" - FlagSummarize = "summarize" - FlagPrintEmptyLine = "printemptyline" - FlagSeconds = "seconds" - FlagAll = "all" - FlagForce = "force" -) - -func Projects(projects []string) []pkg.Project { - result := make([]pkg.Project, len(projects)) - for i, project := range projects { - result[i] = pkg.Project(project) - } - return result -} - -func Users(users []string) []*pkg.User { - result := make([]*pkg.User, len(users)) - for i, user := range users { - result[i] = &pkg.User{ - DisplayName: user, - } - } - return result -} - -type Configuration struct { - Insecure bool `mapstructure:"insecure"` - Host string `mapstructure:"host"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - Year int `mapstructure:"year"` - Month int `mapstructure:"month"` - Projects []string `mapstructure:"projects"` - Users []string `mapstructure:"users"` - Report string `mapstructure:"report"` - Summarize bool `mapstructure:"summarize"` - PrintEmptyLine bool `mapstructure:"printemptyline"` - Seconds bool `mapstructure:"seconds"` -} - -var Config Configuration - -func (c *Configuration) Server() *url.URL { - scheme := "https" - if c.Insecure { - scheme = "http" - } - return &url.URL{ - Scheme: scheme, - Host: c.Host, - } -} - -func (c *Configuration) Userinfo() *url.Userinfo { - if c.Username != "" { - if c.Password != "" { - return url.UserPassword(c.Username, c.Password) - } - return url.User(c.Username) - } - return nil -} - -func (c *Configuration) UsersArray() { - -} diff --git a/internal/configuration.go b/internal/configuration.go new file mode 100644 index 0000000..17a67ea --- /dev/null +++ b/internal/configuration.go @@ -0,0 +1,66 @@ +package internal + +import ( + "net/url" +) + +const ( + Version = "0.1.0" + FlagConfiguration = "configuration" + FlagHttp = "http" + FlagHost = "host" + FlagUsername = "username" + FlagPassword = "password" + FlagYear = "year" + FlagMonth = "month" + FlagProjects = "project" + FlagUsers = "user" + FlagReport = "report" + FlagSummarize = "summarize" + FlagEmpty = "empty" + FlagDecimal = "decimal" + FlagNegate = "negate" + FlagAll = "all" + FlagForce = "force" +) + +type Configuration struct { + Http bool `mapstructure:"http"` + Host string `mapstructure:"host"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Year int `mapstructure:"year"` + Month int `mapstructure:"month"` + Projects []string `mapstructure:"projects"` + Users []string `mapstructure:"users"` + Report string `mapstructure:"report"` + Duration DurationOptions `mapstructure:",squash"` +} + +type DurationOptions struct { + Summarize bool `mapstructure:"summarize"` + Empty bool `mapstructure:"empty"` + Decimal bool `mapstructure:"decimal"` + Negate bool `mapstructure:"negate"` +} + +func (c *Configuration) Server() *url.URL { + scheme := "https" + if c.Http { + scheme = "http" + } + return &url.URL{ + Scheme: scheme, + Host: c.Host, + } +} + +func (c *Configuration) Userinfo() *url.Userinfo { + if c.Username != "" { + if c.Password != "" { + return url.UserPassword(c.Username, c.Password) + } + return url.User(c.Username) + } + return nil +} diff --git a/pkg/csv.go b/pkg/csv.go index 691b2b1..42cba60 100644 --- a/pkg/csv.go +++ b/pkg/csv.go @@ -2,6 +2,7 @@ package pkg import ( "bytes" + "eager/internal" "encoding/csv" "fmt" "io" @@ -157,7 +158,7 @@ func (ts Timesheet) ReadCsv(data []byte, spec *CsvSpecification) (Timesheet, err } } -func (ts Timesheet) WriteCsv(writer io.Writer, spec *CsvSpecification, printEmptyLine, seconds bool) { +func (ts Timesheet) WriteCsv(writer io.Writer, spec *CsvSpecification, opts *internal.DurationOptions) { if len(ts) == 0 { return } @@ -196,13 +197,13 @@ func (ts Timesheet) WriteCsv(writer io.Writer, spec *CsvSpecification, printEmpt for _, effort := range ts { if currentUser != nil && currentUser.DisplayName != effort.User.DisplayName { if currentDate.Day() > 1 { - emptyLinesForDaysBetween(csvw, spec, currentDate, currentDate.AddDate(0, 1, 1-currentDate.Day()), currentUser, seconds) + emptyLinesForDaysBetween(csvw, spec, currentDate, currentDate.AddDate(0, 1, 1-currentDate.Day()), currentUser, opts.Decimal) } currentUser = effort.User currentDate = time.Date(effort.Date.Year(), time.Month(effort.Date.Month()), 1, 0, 0, 0, 0, time.UTC) } - if printEmptyLine { - emptyLinesForDaysBetween(csvw, spec, currentDate, effort.Date, effort.User, seconds) + if opts.Empty { + emptyLinesForDaysBetween(csvw, spec, currentDate, effort.Date, effort.User, opts.Decimal) currentDate = effort.Date.AddDate(0, 0, 1) } @@ -223,8 +224,12 @@ func (ts Timesheet) WriteCsv(writer io.Writer, spec *CsvSpecification, printEmpt } if spec.duration.enabled { var duration string - if seconds { - duration = fmt.Sprintf("%.0f", effort.Duration.Seconds()) + if opts.Decimal { + hours := effort.Duration.Hours() + if opts.Negate { + hours = -hours + } + duration = fmt.Sprintf("%.02f", hours) } else { duration = effort.Duration.String() } @@ -236,13 +241,13 @@ func (ts Timesheet) WriteCsv(writer io.Writer, spec *CsvSpecification, printEmpt log.Println(err) } } - if printEmptyLine && currentDate.Day() > 1 { - emptyLinesForDaysBetween(csvw, spec, currentDate, currentDate.AddDate(0, 1, 1-currentDate.Day()), currentUser, seconds) + if opts.Empty && currentDate.Day() > 1 { + emptyLinesForDaysBetween(csvw, spec, currentDate, currentDate.AddDate(0, 1, 1-currentDate.Day()), currentUser, opts.Decimal) } csvw.Flush() } -func emptyLinesForDaysBetween(csvw *csv.Writer, spec *CsvSpecification, from, to time.Time, user *User, seconds bool) { +func emptyLinesForDaysBetween(csvw *csv.Writer, spec *CsvSpecification, from, to time.Time, user *User, decimal bool) { result := make([]string, spec.fields) if spec.user.enabled && user != nil { result[spec.user.index] = user.DisplayName @@ -251,8 +256,8 @@ func emptyLinesForDaysBetween(csvw *csv.Writer, spec *CsvSpecification, from, to var duration time.Duration duration = 0 var text string - if seconds { - text = fmt.Sprintf("%.0f", duration.Seconds()) + if decimal { + text = fmt.Sprintf("%.02f", duration.Hours()) } else { text = duration.String() } diff --git a/pkg/timesheet.go b/pkg/timesheet.go index 6f25299..f9cfd09 100644 --- a/pkg/timesheet.go +++ b/pkg/timesheet.go @@ -1,6 +1,7 @@ package pkg import ( + "eager/internal" "io" "sort" "strings" @@ -22,6 +23,24 @@ type User struct { TimeZone *time.Location } +func Projects(projects []string) []Project { + result := make([]Project, len(projects)) + for i, project := range projects { + result[i] = Project(project) + } + return result +} + +func Users(users []string) []*User { + result := make([]*User, len(users)) + for i, user := range users { + result[i] = &User{ + DisplayName: user, + } + } + return result +} + type Project string type Task string @@ -104,12 +123,13 @@ func (ts Timesheet) summarize() Timesheet { return timesheet } -func (ts Timesheet) Print(writer io.Writer, user, summarize, printEmptyLine, seconds bool) { +func (ts Timesheet) Print(writer io.Writer, user bool, opts *internal.DurationOptions) { + summarize := opts.Summarize spec := NewCsvSpecification().User(user).Date(true).Project(!summarize).Task(!summarize).Duration(true).Description(!summarize) timesheet := ts if summarize { timesheet = timesheet.summarize() } - timesheet.sortByUserAndDateAndProjectAndTask().WriteCsv(writer, &spec, printEmptyLine, seconds) + timesheet.sortByUserAndDateAndProjectAndTask().WriteCsv(writer, &spec, opts) }